/* $Id: afaudio.c,v 5.1 2001/05/12 18:06:49 bertg Exp $ * * XPilot, a multiplayer gravity war game. Copyright (C) 1991-2001 by * * Bjørn Stabell * Ken Ronny Schouten * Bert Gijsbers * Dick Balaska * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* * History * 1993: AFPlay audio driver by Tom De Pauw . * 1994: Hacked on for AF3 & cached audio by Lance Berc . * 22 Jan 1995: Skip sound headers if present. Tom De Pauw . */ /* * DEC CRL's AudioFile is a public domain network-transparent system * for distributed audio applications and supports Digital RISC * systems running Ultrix, Digital Alpha AXP systems running OSF/1, * Sun Microsystems SPARCstations running SunOS, and SGI Indigos. * It is available via anonymous FTP from crl.dec.com and gatekeeper.dec.com. * * I have finally ported support for AF to xpilot. It works on my DECstation * 5000/25 (a small X window MIPS 3000 based machine) with the following * minor inconveniences (AF experts, please read on): * * 1) Sometimes a sound is lost (never played). Here is my opinion on the * cause. AFPlaySamples needs the audio device time at which the sound * is to be played. Since we want to play right away, we need to query * the AF server for the current time. We then present the sound to be * played with the current time. By that time, there is a chance (xpilot * is quite cpu intensive on DECstation 5000/25) that the current time is * in the past and the sound is thus never played. This should be a lesser * concern on larger cpus. [ I added a 50msec delay to account for scheduling * jitter - lance ] * * 2) Volume. The obvious AFSetOutputGain() control isn't used because * (a) not all devices have hardware gain control, and (b) it doesn't * allow setting the gain on a per-spurt basis. So we now use the software * mixer by setting the ACPlayGain attribute for each spurt. The gain * heuristic is good for the speaker on my J-Video, but your milage may * vary. * * 3) This code skips a possible sound header in Sun or Inverted Sun * format. The rest of the sound file describes samples in 8kHz u-law format. * The sound files suggested for use with xpilot contain the headers. * AF was never intended to read these headers, they would produce * an audible click at the start of each sound. The headers are * assumed of fixed length. This seems the case in the sounds I looked at. * If this causes problem, the header length field will have to be read. * * Resemblance of this code to other programs is not coincidental. * */ #include #include #include #include #include #ifndef _WINDOWS # include #endif #include "version.h" #include "audio.h" char audio_version[] = VERSION; /* Keep a cache of recently played audio. This really helps when the * client has the sound files mounted from a heavily loaded NFS server. */ #define CACHE_SIZE (512 * 1024) #define CACHE_ENTRIES 64 #define SOUND_DELAY 50 /* delay playing a sound for 50msec for slow machines */ struct SoundCache { char *fn; unsigned char *sound; struct timeval when; int length; }; typedef struct { unsigned magic; /* Magic number */ unsigned sample_rate; /* Samples per second */ unsigned samples_per_unit; /* Samples per unit */ unsigned bytes_per_unit; /* Bytes per sample unit */ unsigned channels; /* # interleaved channels */ unsigned encoding; /* Date encoding format */ unsigned info1; /* "info" field of unspecified nature */ unsigned info2; /* (totalling hdr_size - 24) */ } Sun_Audio_Hdr; #define SUN_MAGIC ((unsigned long) 0x2e736e64) /* Really '.snd' */ #define SUN_INV_MAGIC ((unsigned long) 0x646e732e) #define SUN_AUDIO_ENCODING_ULAW (1) static struct SoundCache soundCache[CACHE_ENTRIES]; static int soundCacheEntries = 0; static int soundCacheBytes = 0; static AC ac; static AFAudioConn *aud; static struct timeval now; static int FindDefaultDevice(aud) AFAudioConn *aud; { AFDeviceDescriptor *aDev; int i; char *s; s = (char *) getenv("AF_DEVICE"); if (s != NULL) return(atoi(s)); /* Find the first 8kHz, mono device, non-phone device. */ for (i=0; i < ANumberOfAudioDevices(aud); i++) { aDev = AAudioDeviceDescriptor(aud, i); if ((aDev->inputsFromPhone == 0) && (aDev->outputsToPhone == 0) && (aDev->playSampleFreq == 8000) && (aDev->playNchannels == 1)) return i; } return -1; } int audioDeviceInit(char *display) { AFSetACAttributes attributes; int device; attributes.preempt = Mix; attributes.start_timeout = 0; attributes.end_silence = 0; attributes.play_gain = 0; attributes.rec_gain = 0; attributes.type = MU255; if ((aud = AFOpenAudioConn("")) == NULL) { error ("Cannot open a connection to audio server."); return 1; } device = FindDefaultDevice(aud); if (device == -1) { error ("Cannot find an 8kHz, mono, non-telephone device"); AFCloseAudioConn(aud); return 1; } ac = AFCreateAC(aud, device, ACPlayGain | ACEncodingType, &attributes); /* Success in opening audio device */ return 0; } tossOldestCacheEntry(void) { struct timeval t; struct SoundCache *ce, *oldest; oldest = soundCache; if (oldest->when.tv_sec == 0) oldest->when = now; for (ce = soundCache; ce < &soundCache[CACHE_ENTRIES]; ce++) { if (ce->fn && timercmp(&ce->when, &oldest->when, <)) oldest = ce; } free(oldest->fn); free(oldest->sound); soundCacheBytes -= oldest->length; soundCacheEntries--; bzero((char *) oldest, sizeof(struct SoundCache)); } struct SoundCache *newCacheEntry(char *fn) { int fd; struct stat sbuf; struct SoundCache *ce; Sun_Audio_Hdr header; /* Open the sound file for reading */ if ((fd = open(fn, O_RDONLY, 0)) < 0) { error("Unable to open sound file %s.", fn); return NULL; } if (fstat(fd, &sbuf) == -1) { error("Unable to stat sound file %s.", fn); close(fd); return NULL; } /* If we have a Sun audio file, strip the header and adjust size. */ if (read(fd, (char *)&header, sizeof(Sun_Audio_Hdr)) < sizeof(Sun_Audio_Hdr)) { error("Warning: assuming no header in: %s.", fn); close(fd); fd = open(fn, O_RDONLY, 0); } else if (header.magic != SUN_MAGIC && header.magic != SUN_INV_MAGIC /*|| header.encoding != SUN_AUDIO_ENCODING_ULAW*/ ) { error("Warning: found %x, expected sound header %x or %x in %s.", header.magic, SUN_MAGIC, SUN_INV_MAGIC, fn); close(fd); fd = open(fn, O_RDONLY, 0); } else /* We have in fact found a header */ sbuf.st_size -= sizeof(Sun_Audio_Hdr); /* Truncate huge sound files to the cache size */ if (sbuf.st_size > CACHE_SIZE) sbuf.st_size = CACHE_SIZE; /* If the cache is full, throw out the oldest entry. */ while ((soundCacheEntries == CACHE_ENTRIES) || (soundCacheBytes + sbuf.st_size > CACHE_SIZE)) tossOldestCacheEntry(); /* Find an empty cache entry */ for (ce = soundCache; ce < &soundCache[CACHE_ENTRIES]; ce++) if (!ce->fn) break; ce->fn = malloc(strlen(fn + 1)); strcpy(ce->fn, fn); ce->sound = (unsigned char *) malloc(sbuf.st_size); if (read(fd, ce->sound, sbuf.st_size) != sbuf.st_size) { error("Unable to read sound file %s.", fn); free(ce->sound); free(ce->fn); ce->fn = NULL; ce->sound = NULL; close(fd); return(NULL); } close(fd); ce->length = sbuf.st_size; soundCacheBytes += sbuf.st_size; soundCacheEntries++; return(ce); } struct SoundCache *findCacheEntry(char *fn) { struct SoundCache *ce; for (ce = soundCache ; ce < &soundCache[CACHE_ENTRIES]; ce++) if (ce->fn && !strcmp(fn, ce->fn)) return ce; ce = newCacheEntry(fn); return(ce); } void audioDevicePlay(char *filename, int type, int volume, void **private) { int gain; AFSetACAttributes acAttributes; struct SoundCache *ce; ATime t; if ((ce = findCacheEntry(filename)) == NULL) return; gettimeofday(&now, NULL); ce->when = now; /* this is a hacky, kludgey, way of doing things. yuck. but it works */ /* Convert from 10 - 100 to -25 - -5 */ gain = (((volume - 10) * 20) / 90) - 25; acAttributes.play_gain = gain; AFChangeACAttributes(ac, ACPlayGain, &acAttributes); t = AFGetTime(ac) + (8 * SOUND_DELAY); /* 8 samples/msec */ t = AFPlaySamples(ac, t, ce->length, ce->sound); } void audioDeviceEvents(void) { }