/* * $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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 #include #include #include #include #include #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 #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 */