/* * @(#)cddaslave.c 1.11 13 Sep 1995 * * 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 "libwm/include/wm_config.h" #include "libwm/include/wm_cdda.h" #ifdef BUILD_CDDA /* { */ #include #include #include #ifndef timerclear #define timerclear(tvp) ((tvp)->tv_sec = (tvp)->tv_usec = 0) #endif int playing = 0; /* Should the CD be playing now? */ /* * 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; /* * 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; }; #ifdef BIG_ENDIAN # ifndef htonl # define htonl(x) (x) # endif #else extern unsigned long htonl(x); #endif void *malloc(); long cdda_transform(); /* * Send status information upstream. */ void send_status(struct cdda_block *blk) { 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. * 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). */ void command(int cd_fd, struct cdda_block *blk) { unsigned char inbuf[10]; char *filename; int namelen; struct auheader hdr; if (read(0, inbuf, 1) <= 0) /* Parent died. */ { wmcdda_close(); wmaudio_close(); exit(0); } switch (inbuf[0]) { case 'P': read(0, inbuf, 9); playing = 1; wmaudio_stop(); 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]); wmaudio_ready(); level = 2500; volume = 1 << 15; blk->status = WMCDDA_ACK; send_status(blk); break; case 'S': playing = 0; wmaudio_stop(); blk->status = WMCDDA_ACK; send_status(blk); blk->status = WMCDDA_STOPPED; send_status(blk); break; case 'B': read(0, inbuf, 1); wmaudio_balance(inbuf[0]); blk->status = WMCDDA_ACK; send_status(blk); break; case 'V': read(0, inbuf, 1); wmaudio_volume(inbuf[0]); blk->status = WMCDDA_ACK; send_status(blk); break; case 'G': blk->status = WMCDDA_ACK; send_status(blk); if (playing) blk->status = WMCDDA_PLAYED; else blk->status = WMCDDA_STOPPED; wmaudio_state(blk); send_status(blk); break; case 'Q': blk->status = WMCDDA_ACK; send_status(blk); wmcdda_close(); wmaudio_close(); exit(0); case 's': read(0, inbuf, 1); speed = inbuf[0]; wmcdda_speed(speed); blk->status = WMCDDA_ACK; send_status(blk); break; case 'd': read(0, inbuf, 1); wmcdda_direction(inbuf[0]); blk->status = WMCDDA_ACK; send_status(blk); break; case 'L': read(0, inbuf, 1); loudness = inbuf[0]; blk->status = WMCDDA_ACK; send_status(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(); 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); } blk->status = WMCDDA_ACK; send_status(blk); } } /* * Transform some CDDA data. */ long wmcdda_transform(unsigned char *rawbuf, long buflen, struct cdda_block *block) { long i; long *buf32 = (long *)rawbuf; short *buf16 = (short *)rawbuf; 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); } main(argc, argv) char **argv; { int cd_fd = 3; fd_set readfd, dummyfd; struct timeval timeout; char *cddabuf; long cddabuflen; struct cdda_block blockinfo; long result; int nfds; char *devname; /* * Device name should be the command-line argument. */ if (argc < 2) devname = ""; else devname = argv[1]; /* * If we're running setuid root, bump up our priority then lose * superuser access. */ nice(-14); setgid(getgid()); setuid(getuid()); if (getuid() != geteuid()) return 255; FD_ZERO(&dummyfd); FD_ZERO(&readfd); timerclear(&timeout); cd_fd = wmcdda_init(&cddabuf, &cddabuflen, cd_fd, devname); if (cd_fd < 0) exit(1); wmaudio_init(); blockinfo.status = WMCDDA_ACK; send_status(&blockinfo); blockinfo.status = WMCDDA_STOPPED; fprintf(stderr,"cddaslave: done init."); /* * 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(cd_fd); wmaudio_close(); fprintf(stderr,"cddaslave: Borken pipe; GUI must have exited."); exit(0); } if (FD_ISSET(0, &readfd)) { command(cd_fd, &blockinfo); /* * Process all commands in rapid succession, rather * than possibly waiting for a CDDA read. */ continue; } if (playing) { result = wmcdda_read(cd_fd, cddabuf, cddabuflen, &blockinfo); if (result <= 0) { /* Let the output queue drain. */ if (blockinfo.status == WMCDDA_DONE) { wmaudio_mark_last(); if (wmaudio_send_status()) { /* queue drained, stop polling*/ playing = 0; } } else { playing = 0; send_status(&blockinfo); } } else { result = wmcdda_normalize(cddabuf, result, &blockinfo); result = wmcdda_transform(cddabuf, result, &blockinfo); if (output) fwrite(cddabuf, result, 1, output); result = wmaudio_convert(cddabuf, result, &blockinfo); if (wmaudio_play(cddabuf, result, &blockinfo)) { playing = 0; wmaudio_stop(); send_status(&blockinfo); } } } else send_status(&blockinfo); } } #else /* BUILD_CDDA } { */ #include int main() { printf("cddaslave: will work only on Solaris 2.4 or newer."); exit(0); } #endif /* } */