summaryrefslogtreecommitdiffstats
path: root/kscd/libwm/cddaslave.c
diff options
context:
space:
mode:
Diffstat (limited to 'kscd/libwm/cddaslave.c')
-rw-r--r--kscd/libwm/cddaslave.c572
1 files changed, 572 insertions, 0 deletions
diff --git a/kscd/libwm/cddaslave.c b/kscd/libwm/cddaslave.c
new file mode 100644
index 00000000..9a008b84
--- /dev/null
+++ b/kscd/libwm/cddaslave.c
@@ -0,0 +1,572 @@
+/*
+ * $id$
+ *
+ * This file is part of WorkMan, the civilized CD player library
+ * (c) 1991-1997 by Steven Grimm (original author)
+ * (c) by Dirk Försterling (current 'author' = maintainer)
+ * The maintainer can be contacted by his e-mail address:
+ * milliByte@DeathsDoor.com
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the Free
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ ******************************************************************
+ *
+ * Digital audio manipulator for WorkMan.
+ *
+ * The CDDA architecture looks like this:
+ *
+ * WorkMan (or another UI!)
+ * ^^^
+ * ||| (separate processes connected by pipe)
+ * vvv
+ * +------------- cddaslave -------------+
+ * | | |
+ * command module CDDA reader audio output
+ * (portable) (per platform) (per platform)
+ *
+ * This source file has the command module and some of the scaffolding
+ * to hold cddaslave together, plus some non-system-dependent audio
+ * processing code. Look in plat_*_cdda.c for system-specific stuff.
+ *
+ */
+
+#include "include/wm_cdda.h"
+
+#ifdef BUILD_CDDA
+
+static char cddaslave_id[] = "$Id$";
+
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include "include/wm_struct.h"
+#include "include/wm_cdda.h"
+#include "include/wm_platform.h"
+#include "audio/audio.h"
+
+void send_status(struct cdda_block *);
+
+#define SEND_ACK(b); { (b)->status |= WMCDDA_ACK; send_status(b); }
+#define SEND_STATUS(b); { (b)->status &= ~WMCDDA_ACK; send_status(b); }
+#define SEND_STATUS_ACK(b, s); { (b)->status = ((s) | WMCDDA_ACK); send_status(b); }
+
+int receive_command(struct cdda_device *, struct cdda_block *);
+
+int playing = 0; /* Should the CD be playing now? */
+int pausing = 0;
+
+/*
+ * Loudness setting, plus the floating volume multiplier and decaying-average
+ * volume level.
+ */
+int loudness = 0;
+unsigned int volume = 32768;
+unsigned int level;
+
+/*
+ * Playback speed (0 = slow)
+ */
+int speed = 128;
+
+/*
+ * This is non-null if we're saving audio to a file.
+ */
+FILE *output = NULL;
+
+/*
+ * These are driverdependent oops
+ *
+ */
+struct audio_oops *oops = NULL;
+
+/*
+ * Audio file header format.
+ */
+typedef unsigned long u_32;
+struct auheader {
+ u_32 magic;
+ u_32 hdr_size;
+ u_32 data_size;
+ u_32 encoding;
+ u_32 sample_rate;
+ u_32 channels;
+};
+
+/* had to change #ifdef to #if -> see wm_cdda.h */
+#ifdef __FreeBSD__
+/* Phungus not with htonl on FreeBSD */
+#include <sys/param.h>
+#else
+#if WM_BIG_ENDIAN
+# ifndef htonl
+# define htonl(x) (x)
+# endif
+#else
+extern unsigned long htonl(x);
+#endif
+#endif
+
+void *malloc();
+long cdda_transform();
+
+/*
+ * Send status information upstream.
+ */
+void
+send_status(struct cdda_block *blk)
+{
+ DEBUGLOG("send_status, send %i(%s | %s)\n", blk->status,
+ gen_status(blk->status & WMCDDA_ACK), gen_status(blk->status & ~WMCDDA_ACK));
+ write(1, blk, sizeof(*blk));
+}
+
+/*
+ * Accept a command from our master.
+ *
+ * The protocol is byte-oriented:
+ * PmsfMSFxyz Play from msf to MSF (MSF can be 0,0,0 to play to end)
+ * xyz is the msf of the start of this chunk, i.e., the
+ * ending boundary for reverse play.
+ * S Stop.
+ * E Eject. This means we just close the CD device and
+ * open it again later.
+ * Q Quit.
+ * Vn Set volume level (0-255).
+ * Bn Set balance level (0-255).
+ * EnL Set an equalizer level (n = 0 for bass, 255 for treble)
+ * G Get current status.
+ * sn Set speed multiplier to n.
+ * dn Set direction to forward (n = 0) or reverse.
+ * Fllllx... Start saving to a file (length = l, followed by name)
+ * F0000 Stop saving.
+ * Ln Set loudness level (0-255).
+ * A Pause/Resume
+ * I Get status, current frame
+ */
+int
+receive_command(struct cdda_device *dev, struct cdda_block* blk)
+{
+ unsigned char inbuf[10];
+ char *filename;
+ int namelen;
+ struct auheader hdr;
+
+ if (read(0, inbuf, 1) <= 0) {
+ wmcdda_close(dev);
+ oops->wmaudio_close();
+/* ERRORLOG("cddaslave: parent died, exit\n");*/
+ exit(0);
+ }
+
+ DEBUGLOG("cddaslave: CMD %c\n", inbuf[0]);
+
+ switch (inbuf[0]) {
+ case 'I':
+ if(dev->fd < 0) wmcdda_init(dev, blk);
+ SEND_ACK(blk);
+ break;
+ case 'A': /* pause/resume */
+ if(WMCDDA_PLAYING & blk->status) {
+ oops->wmaudio_stop();
+ SEND_STATUS_ACK(blk, WMCDDA_PAUSED);
+ } else if (WMCDDA_PAUSED & blk->status) {
+ SEND_STATUS_ACK(blk, WMCDDA_PLAYING);
+ } else {
+ SEND_ACK(blk);
+ }
+ break;
+ case 'E':
+ oops->wmaudio_stop();
+ wmcdda_close(dev);
+ SEND_ACK(blk);
+ break;
+ case 'P':
+ read(0, inbuf, 9);
+
+ wmcdda_setup(inbuf[0] * 60 * 75 + inbuf[1] * 75 + inbuf[2],
+ inbuf[3] * 60 * 75 + inbuf[4] * 75 + inbuf[5],
+ inbuf[6] * 60 * 75 + inbuf[7] * 75 + inbuf[8]);
+
+ level = 2500;
+ volume = 1 << 15;
+
+ blk->track = -1;
+ blk->index = 0;
+ blk->minute = inbuf[6];
+ blk->second = inbuf[7];
+ blk->frame = inbuf[8];
+ SEND_STATUS_ACK(blk, WMCDDA_PLAYING);
+ break;
+ case 'S':
+ oops->wmaudio_stop();
+ SEND_STATUS_ACK(blk, WMCDDA_STOPPED);
+ break;
+ case 'B':
+ read(0, inbuf, 1);
+ if(oops->wmaudio_balance)
+ oops->wmaudio_balance(inbuf[0]);
+ break;
+ case 'V':
+ read(0, inbuf, 1);
+ if(oops->wmaudio_balance)
+ oops->wmaudio_volume(inbuf[0]);
+ break;
+ case 'G':
+ SEND_ACK(blk);
+ if(!oops->wmaudio_state || oops->wmaudio_state(blk) == -1) {
+ blk->volume = -1;
+ blk->balance = 128;
+ }
+ send_status(blk);
+ break;
+ case 'Q':
+ SEND_ACK(blk);
+ wmcdda_close(dev);
+ oops->wmaudio_close();
+ exit(0);
+/*
+ case 's':
+ read(0, inbuf, 1);
+ speed = inbuf[0];
+ wmcdda_speed(speed);
+ SEND_STATUS(blk, WMCDDA_ACK);
+ break;
+
+ case 'd':
+ read(0, inbuf, 1);
+ wmcdda_direction(inbuf[0]);
+ SEND_STATUS(blk, WMCDDA_ACK);
+ break;
+*/
+ case 'L':
+ read(0, inbuf, 1);
+ loudness = inbuf[0];
+ SEND_ACK(blk);
+ break;
+ case 'F':
+ read(0, &namelen, sizeof(namelen));
+ if (output != NULL) {
+ fclose(output);
+ output = NULL;
+ }
+ if (namelen) {
+ filename = malloc(namelen + 1);
+ if (filename == NULL) {
+ perror("cddaslave");
+ wmcdda_close(dev);
+ oops->wmaudio_close();
+ exit(1);
+ }
+
+ read(0, filename, namelen);
+ filename[namelen] = '\0';
+ output = fopen(filename, "w");
+ if (output == NULL)
+ perror(filename);
+ else {
+ /* Write an .au file header. */
+ hdr.magic = htonl(0x2e736e64);
+ hdr.hdr_size = htonl(sizeof(hdr) + 28);
+ hdr.data_size = htonl(~0);
+ hdr.encoding = htonl(3); /* linear-16 */
+ hdr.sample_rate = htonl(44100);
+ hdr.channels = htonl(2);
+
+ fwrite(&hdr, sizeof(hdr), 1, output);
+ fwrite("Recorded from CD by WorkMan", 28, 1, output);
+ }
+ free(filename);
+ }
+ SEND_ACK(blk);
+ }
+
+ return(dev->fd);
+}
+
+
+/*
+ * Transform some CDDA data.
+ */
+long
+wmcdda_transform(unsigned char *rawbuf, long buflen, struct cdda_block *block)
+{
+ long i;
+ long *buf32 = (long *)rawbuf;
+ register short *buf16 = (short *)rawbuf;
+ register int aval;
+
+ /*
+ * Loudness transformation. Basically this is a self-adjusting
+ * volume control; our goal is to keep the average output level
+ * around a certain value (2500 seems to be pleasing.) We do this
+ * by maintaining a decaying average of the recent output levels
+ * (where "recent" is some fraction of a second.) All output levels
+ * are multiplied by the inverse of the decaying average; this has
+ * the volume-leveling effect we desire, and isn't too CPU-intensive.
+ *
+ * This is done by modifying the digital data, rather than adjusting
+ * the system volume control, because (at least on some systems)
+ * tweaking the system volume can generate little pops and clicks.
+ *
+ * There's probably a more elegant way to achieve this effect, but
+ * what the heck, I never took a DSP class and am making this up as
+ * I go along, with a little help from some friends.
+ *
+ * This is all done with fixed-point math, oriented around powers
+ * of two, which with luck will keep the CPU usage to a minimum.
+ * More could probably be done, for example using lookup tables to
+ * replace multiplies and divides; whether the memory hit (128K
+ * for each table) is worthwhile is unclear.
+ */
+ if (loudness)
+ {
+ /* We aren't really going backwards, but i > 0 is fast */
+ for (i = buflen / 2; i > 0; i--)
+ {
+ /*
+ * Adjust this sample to the current level.
+ */
+ aval = (*buf16 = (((long)*buf16) * volume) >> 15);
+ buf16++;
+
+ /*
+ * Don't adjust the decaying average for each sample;
+ * that just spends CPU time for very little benefit.
+ */
+ if (i & 127)
+ continue;
+
+ /*
+ * We want to use absolute values to compute the
+ * decaying average; otherwise it'd sit around 0.
+ */
+ if (aval < 0)
+ aval = -aval;
+
+ /*
+ * Adjust more quickly when we start hitting peaks,
+ * or we'll get clipping when there's a sudden loud
+ * section after lots of quiet.
+ */
+ if (aval & ~8191)
+ aval <<= 3;
+
+ /*
+ * Adjust the decaying average.
+ */
+ level = ((level << 11) - level + aval) >> 11;
+
+ /*
+ * Let *really* quiet sounds play softly, or we'll
+ * amplify background hiss to full volume and blast
+ * the user's speakers when real sound starts up.
+ */
+ if (! (level & ~511))
+ level = 512;
+
+ /*
+ * And adjust the volume setting using the inverse
+ * of the decaying average.
+ */
+ volume = (2500 << 15) / level;
+ }
+ }
+
+ if (speed == 128)
+ return (buflen);
+
+ /*
+ * Half-speed play. Stretch things out.
+ */
+ if (speed == 0)
+ {
+ buflen /= 2; /* Since we're moving 32 bits at a time */
+
+ for (i = buflen - 1; i > 0; i--)
+ {
+ buf32[i] = buf32[i / 2];
+ }
+
+ buflen *= 4; /* 2 for doubling the buffer, 2 from above */
+ }
+
+ /*
+ * Slow play; can't optimize it as well as half-speed.
+ */
+ if (speed && speed < 128)
+ {
+ int multiplier = ((speed + 128) * 128) / 256;
+ int newlen;
+ int tally = 0, pos;
+
+ buflen /= 4; /* Get the number of 32-bit values */
+
+ /*
+ * Buffer length doubles when speed is 0, stays the same
+ * when speed is 128.
+ */
+ newlen = (buflen * 128) / multiplier;
+
+ pos = buflen - 1;
+ for (i = newlen - 1; i > 0; i--)
+ {
+ buf32[i] = buf32[pos];
+ tally += multiplier;
+ if (tally & 128)
+ {
+ pos--;
+ tally ^= 128;
+ }
+ }
+
+ buflen = newlen * 4;
+ }
+
+ return (buflen);
+}
+
+
+int main(int argc, char **argv)
+{
+ fd_set readfd, dummyfd;
+ struct timeval timeout;
+ struct cdda_block blockinfo;
+ long result;
+ int nfds;
+ struct cdda_device dev;
+ const char *sondsystem;
+ const char *sounddevice;
+ const char *sounddevicectl;
+
+ memset(&blockinfo, 0, sizeof(struct cdda_block));
+
+ /*
+ * Device name should be the command-line argument.
+ */
+ if (argc < 2)
+ dev.devname = NULL;
+ else
+ dev.devname = argv[1];
+
+ if (argc < 3)
+ sondsystem = "arts";
+ else
+ sondsystem = argv[2];
+
+ if (argc < 4)
+ sounddevice = NULL;
+ else
+ sounddevice = argv[3];
+
+ if (argc < 5)
+ sounddevicectl = NULL;
+ else
+ sounddevicectl = argv[3];
+
+ DEBUGLOG("cddaslave: called with %s %s %s %s\n",
+ dev.devname?dev.devname:"",
+ sondsystem?sondsystem:"",
+ sounddevice?sounddevice:"",
+ sounddevicectl?sounddevicectl:"");
+
+ /*
+ * If we're running setuid root, bump up our priority then lose
+ * superuser access.
+ */
+ nice(-14);
+ setgid(getgid());
+ setuid(getuid());
+ if (geteuid() != getuid())
+ return 255;
+
+ FD_ZERO(&dummyfd);
+ FD_ZERO(&readfd);
+
+ timerclear(&timeout);
+
+ dev.fd = -1;
+ wmcdda_init(&dev, &blockinfo);
+
+ oops = setup_soundsystem(sondsystem, sounddevice, sounddevicectl);
+ if (!oops) {
+ ERRORLOG("cddaslave: setup_soundsystem failed\n");
+ exit(1);
+ }
+
+ DEBUGLOG("cddaslave: sent first ACK\n");
+ SEND_ACK(&blockinfo);
+
+ /*
+ * Accept commands as they come in, and play some sound if we're
+ * supposed to be doing that.
+ */
+ while (1) {
+ FD_SET(0, &readfd);
+
+ /*
+ * If we're playing, we don't want select to block. Otherwise,
+ * wait a little while for the next command.
+ */
+ if (playing)
+ timeout.tv_usec = 0;
+ else
+ timeout.tv_usec = 500000;
+
+ nfds = select(1, &readfd, &dummyfd, &dummyfd, &timeout);
+
+ if (nfds < 0) { /* Broken pipe; our GUI exited. */
+ wmcdda_close(&dev);
+ oops->wmaudio_close();
+ exit(0);
+ }
+
+ if (FD_ISSET(0, &readfd)) {
+ receive_command(&dev, &blockinfo);
+ /*
+ * Process all commands in rapid succession, rather
+ * than possibly waiting for a CDDA read.
+ */
+ continue;
+ }
+
+ if ((blockinfo.status & ~WMCDDA_ACK) == WMCDDA_PLAYING) {
+ result = wmcdda_read(&dev, &blockinfo);
+ if (result <= 0 && blockinfo.status != WMCDDA_DONE) {
+ ERRORLOG("cddaslave: wmcdda_read failed\n");
+ blockinfo.status = WMCDDA_STOPPED;
+ send_status(&blockinfo);
+ } else {
+ result = wmcdda_normalize(&dev, &blockinfo);
+ if (output)
+ fwrite(dev.buf, result, 1, output);
+
+ if (oops->wmaudio_play(dev.buf, dev.buflen, &blockinfo)) {
+ oops->wmaudio_stop();
+ ERRORLOG("cddaslave: wmaudio_play failed\n");
+ blockinfo.status = WMCDDA_STOPPED;
+ send_status(&blockinfo);
+ }
+ }
+ } else
+ send_status(&blockinfo);
+ }
+
+ return 0;
+}
+
+#endif /* BUILD_CDDA */