tech-kern archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

Audio device mmap and kevents



Hello,

I would like to propose a small improvement to the audio system.
I think that it is very interesting to be able to mmap the audio device for better performance and smaller latency but It seems neither clean nor optimal to use
AUDIO_GETOOFFS ioctl and sleep to synchronize.

Why don't we use kernel events?

Something like audio.patch for the kernel.
I also implemented that use in audioplay and tried to clean a bit the code in audioplay.patch.

There is certainly more work to be done but I have to ask now. What do you think?

Thanks.
diff --git a/sys/dev/audio.c b/sys/dev/audio.c
index 49fc0cce9c2e..f0faba543824 100644
--- a/sys/dev/audio.c
+++ b/sys/dev/audio.c
@@ -3453,8 +3453,11 @@ filt_audiowrite(struct knote *kn, long hint)
 	struct audio_chan *chan;
 	audio_stream_t *stream;
 	dev_t dev;
+	struct virtual_channel *vc;
+	int rv = true;
 
 	chan = kn->kn_hook;
+	vc = chan->vc;
 	dev = chan->dev;
 	sc = device_lookup_private(&audio_cd, AUDIOUNIT(dev));
 	if (sc == NULL)
@@ -3463,11 +3466,21 @@ filt_audiowrite(struct knote *kn, long hint)
 	mutex_enter(sc->sc_intr_lock);
 
 	stream = chan->vc->sc_pustream;
-	kn->kn_data = (stream->end - stream->start)
-		- audio_stream_get_used(stream);
+	if (vc->sc_mpr.mmapped) {
+		off_t offset;
+		offset = stream->outp - stream->start
+			+ vc->sc_mpr.blksize;
+		if (stream->start + offset >= stream->end)
+			offset = 0;
+		kn->kn_data = offset;
+	} else {
+		kn->kn_data = (stream->end - stream->start)
+			- audio_stream_get_used(stream);
+		rv = kn->kn_data > 0;
+	}
 	mutex_exit(sc->sc_intr_lock);
 
-	return kn->kn_data > 0;
+	return rv;
 }
 
 static const struct filterops audiowrite_filtops = {
@@ -3816,6 +3829,9 @@ audio_mix(void *v)
 			cb->s.outp = audio_stream_add_outp(&cb->s, cb->s.outp,
 			    blksize);
 			mutex_exit(sc->sc_intr_lock);
+			if ((vc->sc_mode & AUMODE_PLAY) && !cb->pause)
+				sc->schedule_wih = true;
+
 			continue;
 		}
 
@@ -3934,7 +3950,7 @@ audio_mix(void *v)
 	vc = sc->sc_hwvc;
 	cb = &sc->sc_mixring.sc_mpr;
 	inp = cb->s.inp;
-	cc = blksize - (inp - cb->s.start) % blksize;
+	cc = blksize - (inp - cb->s.start) % blksize; // that line in 'then' branch
 	if (sc->sc_writeme == false) {
 		DPRINTFN(3, ("MIX RING EMPTY - INSERT SILENCE\n"));
 		audio_fill_silence(&vc->sc_pustream->param, inp, cc);
diff --git a/usr.bin/audio/play/audioplay.1 b/usr.bin/audio/play/audioplay.1
index 59ea9c4ded7f..7ff60217ba37 100644
--- a/usr.bin/audio/play/audioplay.1
+++ b/usr.bin/audio/play/audioplay.1
@@ -118,6 +118,12 @@ sample rate.
 Print a help message.
 .It Fl i
 If the audio device cannot be opened, exit now rather than wait for it.
+.It Fl l
+List the audio encodings supported by the device and if it emulated
+or not. Useful to know what format to use when using
+.Fl m .
+.It Fl m
+Use mmap on the audio device.
 .It Fl P
 When combined with the
 .Fl f
diff --git a/usr.bin/audio/play/play.c b/usr.bin/audio/play/play.c
index b273a6ff94db..790608f57595 100644
--- a/usr.bin/audio/play/play.c
+++ b/usr.bin/audio/play/play.c
@@ -37,6 +37,8 @@ __RCSID("$NetBSD: play.c,v 1.56 2018/11/16 13:55:17 mlelstv Exp $");
 #include <sys/ioctl.h>
 #include <sys/mman.h>
 #include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/event.h>
 
 #include <err.h>
 #include <fcntl.h>
@@ -46,16 +48,20 @@ __RCSID("$NetBSD: play.c,v 1.56 2018/11/16 13:55:17 mlelstv Exp $");
 #include <string.h>
 #include <unistd.h>
 #include <util.h>
+#include <stdarg.h>
 
 #include <paths.h>
 
 #include "libaudio.h"
 
 static void usage(void) __dead;
-static void play(char *);
-static void play_fd(const char *, int);
+static void play(const char *);
 static ssize_t audioctl_write_fromhdr(void *, size_t, int, off_t *, const char *);
 static void cleanup(int) __dead;
+static int audio_write(void *, size_t);
+//static void s_err_aux(void (*wf)(char const*, va_list), int, const char*, va_list);
+static void s_errx(int, const char*, ...);
+static void s_err(int, const char*, ...);
 
 static audio_info_t	info;
 static int	volume;
@@ -63,18 +69,65 @@ static int	balance;
 static int	port;
 static int	fflag;
 static int	qflag;
-int	verbose;
+int		verbose = 1;
 static int	sample_rate;
 static int	encoding;
 static char	*encoding_str;
 static int	precision;
 static int	channels;
+static int	audio_memmap = 0;
+static int 	file_memmap = 1;
+static void	*oaddr = NULL;
+static size_t	sizet_filesize;
 
 static char	const *play_errstring = NULL;
 static size_t	bufsize;
-static int	audiofd;
+static int	audiofd = -1;
 static int	exitstatus = EXIT_SUCCESS;
 
+typedef struct audiomm {
+	int kq;
+	void* addr;
+	size_t size;
+	char* inp; //addr to be written next
+	char* outp;
+} audiomm_t;
+static audiomm_t g_aumm;
+
+static int
+init_aumm(audiomm_t *aumm)
+{
+	struct kevent ev;
+
+	aumm->addr = mmap(0, bufsize, PROT_WRITE, MAP_SHARED, audiofd, 0);
+	if (aumm->addr == MAP_FAILED)
+		return -1;
+	(void)madvise(aumm->addr, bufsize, MADV_SEQUENTIAL);
+	aumm->size = bufsize;
+
+	if ((aumm->kq = kqueue()) == -1) {
+		warn("kqueue");
+		goto cant_mmap;
+	}
+	EV_SET(&ev, audiofd, EVFILT_WRITE, EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, 0);
+	if (kevent(aumm->kq, &ev, 1, NULL, 0, NULL) == -1) {
+		warn("kevent");
+		goto cant_mmap;
+	}
+	if (kevent(aumm->kq, NULL, 0, &ev, 1, NULL) == -1) {
+		warn("kevent");
+		goto cant_mmap;
+	}
+	aumm->inp = aumm->outp = (char*)aumm->addr + ev.data;
+
+	return 0;
+cant_mmap:
+	if (munmap(aumm->addr, bufsize) < 0)
+		warn("munmap");
+	aumm->addr = MAP_FAILED;
+	return -1;
+}
+
 int
 main(int argc, char *argv[])
 {
@@ -83,13 +136,14 @@ main(int argc, char *argv[])
 	int	iflag = 0;
 	const char *defdevice = _PATH_SOUND;
 	const char *device = NULL;
+	int	list_audio_encodings = 0;
 
-	while ((ch = getopt(argc, argv, "b:B:C:c:d:e:fhip:P:qs:Vv:")) != -1) {
+	while ((ch = getopt(argc, argv, "b:B:C:c:d:e:fhilmp:P:qs:Vv:")) != -1) {
 		switch (ch) {
 		case 'b':
 			decode_int(optarg, &balance);
 			if (balance < 0 || balance > 64)
-				errx(1, "balance must be between 0 and 63");
+				s_errx(EXIT_FAILURE, "balance must be between 0 and 63");
 			break;
 		case 'B':
 			bufsize = strsuftoll("write buffer size", optarg,
@@ -98,7 +152,7 @@ main(int argc, char *argv[])
 		case 'c':
 			decode_int(optarg, &channels);
 			if (channels < 0)
-				errx(1, "channels must be positive");
+				s_errx(EXIT_FAILURE, "channels must be positive");
 			break;
 		case 'C':
 			/* Ignore, compatibility */
@@ -115,6 +169,12 @@ main(int argc, char *argv[])
 		case 'i':
 			iflag++;
 			break;
+		case 'l':
+			list_audio_encodings = 1;
+			break;
+		case 'm':
+			audio_memmap = 1;
+			break;
 		case 'q':
 			qflag++;
 			break;
@@ -123,7 +183,7 @@ main(int argc, char *argv[])
 			if (precision != 4 && precision != 8 &&
 			    precision != 16 && precision != 24 &&
 			    precision != 32)
-				errx(1, "precision must be between 4, 8, 16, 24 or 32");
+				s_errx(EXIT_FAILURE, "precision must be between 4, 8, 16, 24 or 32");
 			break;
 		case 'p':
 			len = strlen(optarg);
@@ -135,13 +195,13 @@ main(int argc, char *argv[])
 			else if (strncmp(optarg, "line", len) == 0)
 				port |= AUDIO_LINE_OUT;
 			else
-				errx(1,
+				s_errx(EXIT_FAILURE,
 			    "port must be `speaker', `headphone', or `line'");
 			break;
 		case 's':
 			decode_int(optarg, &sample_rate);
 			if (sample_rate < 0 || sample_rate > 48000 * 2)	/* XXX */
-				errx(1, "sample rate must be between 0 and 96000");
+				s_errx(EXIT_FAILURE, "sample rate must be between 0 and 96000");
 			break;
 		case 'V':
 			verbose++;
@@ -149,7 +209,7 @@ main(int argc, char *argv[])
 		case 'v':
 			volume = atoi(optarg);
 			if (volume < 0 || volume > 255)
-				errx(1, "volume must be between 0 and 255");
+				s_errx(EXIT_FAILURE, "volume must be between 0 and 255");
 			break;
 		/* case 'h': */
 		default:
@@ -163,7 +223,7 @@ main(int argc, char *argv[])
 	if (encoding_str) {
 		encoding = audio_enc_to_val(encoding_str);
 		if (encoding == -1)
-			errx(1, "unknown encoding, bailing...");
+			s_errx(EXIT_FAILURE, "unknown encoding, bailing...");
 	}
 
 	if (device == NULL && (device = getenv("AUDIODEVICE")) == NULL &&
@@ -177,14 +237,29 @@ main(int argc, char *argv[])
 	}
 
 	if (audiofd < 0)
-		err(1, "failed to open %s", device);
+		s_err(EXIT_FAILURE, "failed to open %s", device);
 
 	if (ioctl(audiofd, AUDIO_GETINFO, &info) < 0)
-		err(1, "failed to get audio info");
+		s_err(EXIT_FAILURE, "failed to get audio info");
 	if (bufsize == 0) {
 		bufsize = info.play.buffer_size;
 		if (bufsize < 32 * 1024)
-			bufsize = 32 * 1024;
+			bufsize = 32 * 1024; //XXX: when audio_memmap, use this as size, do something about that!
+	}
+
+	if (list_audio_encodings) {
+		audio_encoding_t ae = { .index = 0 };
+
+		while (-1 != ioctl(audiofd, AUDIO_GETENC, &ae)) {
+			printf("%d: %s (%d bits)", ae.index, ae.name, ae.precision);
+			if (ae.flags & AUDIO_ENCODINGFLAG_EMULATED)
+				printf(" emulated\n");
+			else
+				printf("\n");
+			++ae.index;
+		}
+
+		cleanup(0);
 	}
 
 	signal(SIGINT, cleanup);
@@ -196,72 +271,119 @@ main(int argc, char *argv[])
 			play(*argv++);
 		while (*argv);
 	else
-		play_fd("standard input", STDIN_FILENO);
+		play("-");
 
 	cleanup(0);
 }
 
 static void
-cleanup(int signo)
+s_err_aux(void (*warn_func)(const char*, va_list), int e, const char *fmt, va_list ap)
+{
+	exitstatus = e;
+	(*warn_func)(fmt, ap);
+	cleanup(0);
+}
+
+static void
+s_err(int e, const char* fmt, ...)
 {
+	va_list ap;
 
-	(void)ioctl(audiofd, AUDIO_FLUSH, NULL);
-	(void)ioctl(audiofd, AUDIO_SETINFO, &info);
-	close(audiofd);
-	if (signo != 0) {
-		(void)raise_default_signal(signo);
+	va_start(ap, fmt);
+	s_err_aux(vwarn, e, fmt, ap);
+	va_end(ap);
+}
+
+static void
+s_errx(int e, const char* fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	s_err_aux(vwarnx, e, fmt, ap);
+	va_end(ap);
+}
+
+static void
+cleanup(int signo)
+{
+	if (audiofd != -1) {
+		(void)ioctl(audiofd, AUDIO_FLUSH, NULL);
+		(void)ioctl(audiofd, AUDIO_SETINFO, &info);
+		if (file_memmap) {
+			if (oaddr != MAP_FAILED)
+				(void)munmap(oaddr, sizet_filesize);
+		else if (oaddr)
+			free(oaddr);
+		}
+		close(audiofd);
 	}
+	if (audio_memmap && g_aumm.addr != MAP_FAILED)
+		(void)munmap(g_aumm.addr, g_aumm.size);
+	if (signo != 0)
+		(void)raise_default_signal(signo);
 	exit(exitstatus);
 }
 
 static void
-play(char *file)
+play(const char *file)
 {
 	struct stat sb;
-	void *addr, *oaddr;
+	char* addr;
 	off_t	filesize;
-	size_t	sizet_filesize;
 	off_t datasize = 0;
 	ssize_t	hdrlen;
-	int fd;
+	int fd, nr = 0, nw;
+	off_t dataout = 0;
 
 	if (file[0] == '-' && file[1] == 0) {
-		play_fd("standard input", STDIN_FILENO);
-		return;
-	}
-
-	fd = open(file, O_RDONLY);
-	if (fd < 0) {
-		if (!qflag)
-			warn("could not open %s", file);
-		exitstatus = EXIT_FAILURE;
-		return;
-	}
-
-	if (fstat(fd, &sb) < 0)
-		err(1, "could not fstat %s", file);
-	filesize = sb.st_size;
-	sizet_filesize = (size_t)filesize;
+		fd = STDIN_FILENO;
+		file = "standard input";
+		file_memmap = 0;
+	} else {
+		fd = open(file, O_RDONLY);
+		if (fd < 0) {
+			if (!qflag)
+				warn("could not open %s", file);
+			return;
+		}
 
-	/*
-	 * if the file is not a regular file, doesn't fit in a size_t,
-	 * or if we failed to mmap the file, try to read it instead, so
-	 * that filesystems, etc, that do not support mmap() work
-	 */
-	if (!S_ISREG(sb.st_mode) || 
-	    ((off_t)sizet_filesize != filesize) ||
-	    (oaddr = addr = mmap(0, sizet_filesize, PROT_READ,
-	    MAP_SHARED, fd, 0)) == MAP_FAILED) {
-		play_fd(file, fd);
-		close(fd);
-		return;
+		if (fstat(fd, &sb) < 0)
+			s_err(EXIT_FAILURE, "could not fstat %s", file);
+		filesize = sb.st_size;
+		sizet_filesize = (size_t)filesize;
+		/*
+		 * if the file is not a regular file, doesn't fit in a size_t,
+		 * or if we failed to mmap the file, try to read it instead, so
+		 * that filesystems, etc, that do not support mmap() work
+		 */
+		if (!S_ISREG(sb.st_mode) ||
+			((off_t)sizet_filesize != filesize) ||
+			(oaddr = addr = mmap(0, sizet_filesize, PROT_READ,
+			MAP_SHARED, fd, 0)) == MAP_FAILED) {
+			if (!qflag)
+				warn("Can't mmap %s\n", file);
+			file_memmap = 0;
+		}
 	}
-
+	if (!file_memmap) {
+			oaddr = addr = malloc(bufsize);
+		if (addr == NULL)
+			s_err(EXIT_FAILURE, "malloc of read buffer failed");
+		nr = read(fd, addr, bufsize);
+		if (nr < 0)
+			s_err(EXIT_FAILURE, "standard input read error");
+		if (nr == 0)
+			s_err(EXIT_FAILURE, "unexpected EOF");
+		filesize = nr;
+		sizet_filesize = (size_t)filesize;
+	} else {
 	/*
 	 * give the VM system a bit of a hint about the type
 	 * of accesses we will make.  we don't care about errors.
 	 */
-	madvise(addr, sizet_filesize, MADV_SEQUENTIAL);
+		madvise(addr, sizet_filesize, MADV_SEQUENTIAL);
+	}
 
 	/*
 	 * get the header length and set up the audio device
@@ -269,97 +391,64 @@ play(char *file)
 	if ((hdrlen = audioctl_write_fromhdr(addr,
 	    sizet_filesize, audiofd, &datasize, file)) < 0) {
 		if (play_errstring)
-			errx(1, "%s: %s", play_errstring, file);
+			s_errx(EXIT_FAILURE, "%s: %s", play_errstring, file);
 		else
-			errx(1, "unknown audio file: %s", file);
+			s_errx(EXIT_FAILURE, "unknown audio file: %s", file);
 	}
-
-	filesize -= hdrlen;
-	addr = (char *)addr + hdrlen;
-	if (filesize < datasize || datasize == 0) {
-		if (filesize < datasize)
-			warnx("bogus datasize: %ld", (u_long)datasize);
-		datasize = filesize;
+	if (!file_memmap) {
+		if (hdrlen > 0) {
+			if (hdrlen > nr)	/* shouldn't happen */
+				s_errx(EXIT_FAILURE, "header seems really large: %lld", (long long)hdrlen);
+			memmove(addr, addr + hdrlen, nr - hdrlen);
+			 nr -= hdrlen;
+			 filesize = nr;
+		}
+	} else {
+		filesize -= hdrlen;
+		addr = addr + hdrlen;
+		if (filesize < datasize || datasize == 0) {
+			if (filesize < datasize)
+				warnx("bogus datasize: %ld", (u_long)datasize);
+			datasize = filesize;
+		}
 	}
 
-	while ((uint64_t)datasize > bufsize) {
-		if ((size_t)write(audiofd, addr, bufsize) != bufsize)
-			err(1, "write failed");
-		addr = (char *)addr + bufsize;
-		datasize -= bufsize;
+	if (audio_memmap && init_aumm(&g_aumm) == -1)
+		audio_memmap = 0;
+
+	if (file_memmap) {
+		while (datasize) {
+			nr = MIN(bufsize, datasize);
+			nw = audio_write(addr, nr);
+			if (nw != nr)
+				s_err(EXIT_FAILURE, "write failed");
+			addr = addr + bufsize;
+			datasize -= nr;
+		}
+	} else {
+		while (datasize == 0 || dataout < datasize) {
+			if (datasize != 0 && dataout + nr > datasize)
+				nr = datasize - dataout;
+			nw = audio_write(addr, nr);
+			if (nw != nr)
+				s_err(EXIT_FAILURE, "write failed");
+			dataout += nw;
+			nr = read(fd, addr, bufsize);
+			if (nr == -1)
+				s_err(EXIT_FAILURE, "read failed");
+			if (nr == 0)
+				break;
+		}
 	}
-	if ((off_t)write(audiofd, addr, datasize) != datasize)
-		err(1, "final write failed");
 
 	if (ioctl(audiofd, AUDIO_DRAIN) < 0 && !qflag)
 		warn("audio drain ioctl failed");
-	if (munmap(oaddr, sizet_filesize) < 0)
-		err(1, "munmap failed");
+	if (file_memmap && munmap(oaddr, sizet_filesize) < 0)
+		s_err(EXIT_FAILURE, "munmap failed");
 
 	close(fd);
 }
 
-/*
- * play the file on the file descriptor fd
- */
-static void
-play_fd(const char *file, int fd)
-{
-	char    *buffer = malloc(bufsize);
-	ssize_t hdrlen;
-	int     nr, nw;
-	off_t	datasize = 0;
-	off_t	dataout = 0;
-
-	if (buffer == NULL)
-		err(1, "malloc of read buffer failed");
-
-	nr = read(fd, buffer, bufsize);
-	if (nr < 0)
-		goto read_error;
-	if (nr == 0) {
-		if (fflag) {
-			free(buffer);
-			return;
-		}
-		err(1, "unexpected EOF");
-	}
-	hdrlen = audioctl_write_fromhdr(buffer, nr, audiofd, &datasize, file);
-	if (hdrlen < 0) {
-		if (play_errstring)
-			errx(1, "%s: %s", play_errstring, file);
-		else
-			errx(1, "unknown audio file: %s", file);
-	}
-	if (hdrlen > 0) {
-		if (hdrlen > nr)	/* shouldn't happen */
-			errx(1, "header seems really large: %lld", (long long)hdrlen);
-		memmove(buffer, buffer + hdrlen, nr - hdrlen);
-		nr -= hdrlen;
-	}
-	while (datasize == 0 || dataout < datasize) {
-		if (datasize != 0 && dataout + nr > datasize)
-			nr = datasize - dataout;
-		nw = write(audiofd, buffer, nr);
-		if (nw != nr)
-			goto write_error;
-		dataout += nw;
-		nr = read(fd, buffer, bufsize);
-		if (nr == -1)
-			goto read_error;
-		if (nr == 0)
-			break;
-	}
-	/* something to think about: no message given for dataout < datasize */
-	if (ioctl(audiofd, AUDIO_DRAIN) < 0 && !qflag)
-		warn("audio drain ioctl failed");
-	return;
-read_error:
-	err(1, "read of standard input failed");
-write_error:
-	err(1, "audio device write failed");
-}
-
 /*
  * only support sun and wav audio files so far ...
  *
@@ -451,16 +540,95 @@ set_audio_mode:
 	}
 
 	if (ioctl(fd, AUDIO_SETINFO, &info) < 0)
-		err(1, "failed to set audio info");
+		s_err(EXIT_FAILURE, "failed to set audio info");
 
 	return (hdr_len);
 }
 
+static char*
+circular_write(audiomm_t* aumm, char* raddr, size_t size)
+{
+	char* end_w = aumm->inp + size;
+	size_t partsize;
+
+	if (end_w > (char*)aumm->addr + aumm->size) {
+		end_w = (char*)aumm->addr + aumm->size;
+		partsize = end_w - aumm->inp;
+		memcpy(aumm->inp, raddr, partsize);
+		raddr += partsize;
+		size -= partsize;
+		aumm->inp = (char*)aumm->addr;
+		end_w = aumm->inp + size;
+	}
+
+	memcpy(aumm->inp, raddr, size);
+	aumm->inp += size;
+
+	return (raddr + size);
+}
+
+static u_int
+aumm_space(audiomm_t *aumm)
+{
+	if (aumm->outp >= aumm->inp)
+		 return aumm->outp - aumm->inp;
+	return aumm->outp + aumm->size - aumm->inp;
+}
+
+static int
+update_aumm(audiomm_t *aumm)
+{
+	struct kevent ev;
+	int nev;
+
+	nev = kevent(aumm->kq, NULL, 0, &ev, 1, NULL);
+	if (nev == -1) {
+		warn("kevent");
+		return nev;
+	}
+	if (ev.data == (int64_t)bufsize)
+		aumm->outp = (char*)aumm->addr;
+	else
+		aumm->outp = (char*)aumm->addr + ev.data;
+
+	return 0;
+}
+
+static int
+audio_write_mmap(audiomm_t *aumm, void* buffer, size_t nbytes)
+{
+	int written = 0;
+	size_t size;
+
+	while (nbytes) {
+		if ((size = aumm_space(aumm)) == 0) {
+			if (update_aumm(aumm)) {
+				warn("update_aumm");
+				return -1;
+			}
+		}
+		size = MIN(size, nbytes);
+		buffer = circular_write(aumm, buffer, size);
+		nbytes -= size;
+		written += size;
+	}
+
+	return written;
+}
+
+static int
+audio_write(void* buffer, size_t nbytes)
+{
+	if (audio_memmap)
+		return audio_write_mmap(&g_aumm, buffer, nbytes);
+	return write(audiofd, buffer, nbytes);
+}
+
 static void
 usage(void)
 {
 
-	fprintf(stderr, "Usage: %s [-hiqV] [options] files\n", getprogname());
+	fprintf(stderr, "Usage: %s [-hilmqV] [options] files\n", getprogname());
 	fprintf(stderr, "Options:\n\t"
 	    "-B buffer size\n\t"
 	    "-b balance (0-63)\n\t"


Home | Main Index | Thread Index | Old Index