/* * Copyright(c) 1997-2001 Id Software, Inc. * Copyright(c) 2002 The Quakeforge Project. * Copyright(c) 2006 Quetoo. * * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "client.h" void S_Update_(); void S_Stop_f(void); // only begin attenuating sound volumes when outside the FULLVOLUME range #define SOUND_FULLVOLUME 50 #define SOUND_LOOPATTENUATE 0.003 int s_registration_sequence; channel_t channels[MAX_CHANNELS]; static qboolean snd_initialized; static qboolean s_registering; dma_t dma; vec3_t listener_origin; vec3_t listener_forward; vec3_t listener_right; vec3_t listener_up; int soundtime; // sample pairs int paintedtime; // sample pairs // during registration it is possible to have more sounds // than could actually be referenced during gameplay, // because we don't want to free anything until we are // sure we won't need it. #define MAX_SFX (MAX_SOUNDS * 2) sfx_t known_sfx[MAX_SFX]; int num_sfx; #define MAX_PLAYSOUNDS 128 playsound_t s_playsounds[MAX_PLAYSOUNDS]; playsound_t s_freeplays; playsound_t s_pendingplays; cvar_t *s_volume; cvar_t *s_loadas8bit; cvar_t *s_mixahead; cvar_t *s_bits; cvar_t *s_rate; cvar_t *s_channels; cvar_t *s_device; cvar_t *s_ambient; cvar_t *s_testsound; cvar_t *s_show; int s_beginofs; int s_rawend; portable_samplepair_t s_rawsamples[MAX_RAW_SAMPLES]; /* S_Info_f */ void S_Info_f(void){ if(!snd_initialized){ Com_Printf("Sound system not started.\n"); return; } Com_Printf("%5d channels\n", dma.channels); Com_Printf("%5d samples\n", dma.samples); Com_Printf("%5d offset\n", dma.offset); Com_Printf("%5d bits\n", dma.bits); Com_Printf("%5d chunk\n", dma.chunk); Com_Printf("%5d speed\n", dma.rate); Com_Printf("0x%lx dma buffer\n", (unsigned long)dma.buffer); } /* S_Play_f */ void S_Play_f(void){ int i; char name[256]; sfx_t *sfx; i = 1; while(i < Cmd_Argc()){ if(!strrchr(Cmd_Argv(i), '.')){ strcpy(name, Cmd_Argv(i)); strcat(name, ".wav"); } else strcpy(name, Cmd_Argv(i)); sfx = S_RegisterSound(name); S_StartSound(NULL, cl.playernum + 1, 0, sfx, 1.0, 1.0, 0); i++; } } /* S_List_f */ void S_List_f(void){ int i; sfx_t *sfx; sfxcache_t *sc; int size, total; total = 0; for(sfx = known_sfx, i = 0; i < num_sfx; i++, sfx++){ if(!sfx->registration_sequence) continue; sc = sfx->cache; if(sc){ size = sc->length * sc->width *(sc->stereo + 1); total += size; if(sc->loopstart >= 0) Com_Printf("L"); else Com_Printf(" "); Com_Printf("(%2db) %6i : %s\n", sc->width*8, size, sfx->name); } else { if(sfx->name[0] == '*') Com_Printf(" placeholder : %s\n", sfx->name); else Com_Printf(" not loaded : %s\n", sfx->name); } } Com_Printf("Total resident: %i\n", total); } /* S_Reload_f */ void S_Reload_f(void){ S_RegisterSounds(); } /* S_Restart_f */ void S_Restart_f(void){ S_Shutdown(); S_Init(); S_Reload_f(); } /* S_Init */ void S_Init(void){ if(Cvar_VariableValue("s_disable")){ Com_Printf("Sound disabled.\n"); return; } Com_Printf("Sound initialization..\n"); s_volume = Cvar_Get("s_volume", "0.7", CVAR_ARCHIVE); s_loadas8bit = Cvar_Get("s_loadas8bit", "0", CVAR_ARCHIVE); s_mixahead = Cvar_Get("s_mixahead", "0.05", CVAR_ARCHIVE); s_bits = Cvar_Get("s_bits", "16", CVAR_ARCHIVE); s_rate = Cvar_Get("s_rate", "", CVAR_ARCHIVE); s_channels = Cvar_Get("s_channels", "2", CVAR_ARCHIVE); s_device = Cvar_Get("s_device", "default", CVAR_ARCHIVE); s_ambient = Cvar_Get("s_ambient", "1", CVAR_ARCHIVE); s_testsound = Cvar_Get("s_testsound", "0", 0); s_show = Cvar_Get("s_show", "0", 0); Cmd_AddCommand("s_restart", S_Restart_f); Cmd_AddCommand("s_reload", S_Reload_f); Cmd_AddCommand("s_play", S_Play_f); Cmd_AddCommand("s_stop", S_Stop_f); Cmd_AddCommand("s_list", S_List_f); Cmd_AddCommand("s_info", S_Info_f); S_InitScaletable(); num_sfx = 0; soundtime = 0; paintedtime = 0; S_Stop_f(); snd_initialized = SND_Init(); Com_Printf("Sound initialized %dbit %dKHz %dch.\n", dma.bits, (dma.rate / 1000), dma.channels); } /* S_Shutdown */ void S_Shutdown(void){ int i; sfx_t *sfx; if(!snd_initialized) return; SND_Shutdown(); snd_initialized = false; Cmd_RemoveCommand("s_play"); Cmd_RemoveCommand("s_stop"); Cmd_RemoveCommand("s_list"); Cmd_RemoveCommand("s_info"); // free all sounds for(i = 0, sfx = known_sfx; i < num_sfx; i++, sfx++){ if(!sfx->name[0]) continue; if(sfx->cache) Z_Free(sfx->cache); memset(sfx, 0, sizeof(*sfx)); } num_sfx = 0; } /* S_FindName */ sfx_t *S_FindName(char *name, qboolean create){ int i; sfx_t *sfx; if(!name) Com_Error(ERR_FATAL, "S_FindName: NULL\n"); if(!name[0]) Com_Error(ERR_FATAL, "S_FindName: empty name\n"); if(strlen(name) >= MAX_QPATH) Com_Error(ERR_FATAL, "Sound name too long: %s", name); // see if already loaded for(i = 0; i < num_sfx; i++) if(!strcmp(known_sfx[i].name, name)) return &known_sfx[i]; if(!create) return NULL; // find a free sfx for(i = 0; i < num_sfx; i++) if(!known_sfx[i].name[0]) break; if(i == num_sfx){ if(num_sfx == MAX_SFX) Com_Error(ERR_FATAL, "S_FindName: out of sfx_t"); num_sfx++; } sfx = &known_sfx[i]; memset(sfx, 0, sizeof(*sfx)); strcpy(sfx->name, name); sfx->registration_sequence = s_registration_sequence; return sfx; } /* S_AliasName */ sfx_t *S_AliasName(char *aliasname, char *truename){ sfx_t *sfx; char *s; int i; s = Z_Malloc(MAX_QPATH); strcpy(s, truename); // find a free sfx for(i = 0; i < num_sfx; i++) if(!known_sfx[i].name[0]) break; if(i == num_sfx){ if(num_sfx == MAX_SFX) Com_Error(ERR_FATAL, "S_FindName: out of sfx_t"); num_sfx++; } sfx = &known_sfx[i]; memset(sfx, 0, sizeof(*sfx)); strcpy(sfx->name, aliasname); sfx->registration_sequence = s_registration_sequence; sfx->truename = s; return sfx; } /* S_BeginRegistration */ void S_BeginRegistration(void){ s_registration_sequence++; s_registering = true; } /* S_RegisterSound */ sfx_t *S_RegisterSound(char *name){ sfx_t *sfx; if(!snd_initialized) return NULL; sfx = S_FindName(name, true); sfx->registration_sequence = s_registration_sequence; if(!s_registering) S_LoadSound(sfx); return sfx; } /* S_EndRegistration */ void S_EndRegistration(void){ int i; sfx_t *sfx; // free any sounds not from this registration sequence for(i = 0, sfx = known_sfx; i < num_sfx; i++, sfx++){ if(!sfx->name[0]) continue; if(sfx->registration_sequence != s_registration_sequence){ // don't need this sound if(sfx->cache) // it is possible to have a leftover Z_Free(sfx->cache); // from a server that didn't finish loading memset(sfx, 0, sizeof(*sfx)); } } // load everything in for(i = 0, sfx = known_sfx; i < num_sfx; i++, sfx++){ if(!sfx->name[0]) continue; S_LoadSound(sfx); } s_registering = false; } /* S_RegisterSounds */ void S_RegisterSounds(void){ int i; S_BeginRegistration(); CL_RegisterTEntSounds(); for(i = 1; i < MAX_SOUNDS; i++){ if(!cl.configstrings[CS_SOUNDS + i][0]) break; cl.sound_precache[i] = S_RegisterSound(cl.configstrings[CS_SOUNDS + i]); } S_EndRegistration(); } /* S_PickChannel */ channel_t *S_PickChannel(int entnum, int entchannel){ int ch_idx; int first_to_die; int life_left; channel_t *ch; if(entchannel < 0) Com_Error(ERR_DROP, "S_PickChannel: entchannel < 0"); // Check for replacement sound, or find the best one to replace first_to_die = -1; life_left = 0x7fffffff; for(ch_idx = 0; ch_idx < MAX_CHANNELS; ch_idx++){ if(entchannel != 0 // channel 0 never overrides && channels[ch_idx].entnum == entnum && channels[ch_idx].entchannel == entchannel){ // always override sound from same entity first_to_die = ch_idx; break; } // don't let monster sounds override player sounds if(channels[ch_idx].entnum == cl.playernum + 1 && entnum != cl.playernum + 1 && channels[ch_idx].sfx) continue; if(channels[ch_idx].end - paintedtime < life_left){ life_left = channels[ch_idx].end - paintedtime; first_to_die = ch_idx; } } if(first_to_die == -1) return NULL; ch = &channels[first_to_die]; memset(ch, 0, sizeof(*ch)); return ch; } /* S_SpatializeOrigin Used for spatializing channels and autosounds */ void S_SpatializeOrigin(vec3_t origin, float master_vol, float dist_mult, int *left_vol, int *right_vol){ vec_t dot; vec_t dist; vec_t lscale, rscale, scale; vec3_t source_vec; if(cls.state != ca_active){ *left_vol = *right_vol = 255; return; } // calculate stereo seperation and distance attenuation VectorSubtract(origin, listener_origin, source_vec); dist = VectorNormalize(source_vec); dist -= SOUND_FULLVOLUME; if(dist < 0) dist = 0; // close enough to be at full volume dist *= dist_mult; // different attenuation levels dot = DotProduct(listener_right, source_vec); if(dma.channels == 1 || !dist_mult){ // no attenuation = no spatialization rscale = 1.0; lscale = 1.0; } else { rscale = 0.5 *(1.0 + dot); lscale = 0.5 *(1.0 - dot); } // add in distance effect scale =(1.0 - dist) * rscale; *right_vol =(int)(master_vol * scale); if(*right_vol < 0) *right_vol = 0; scale =(1.0 - dist) * lscale; *left_vol =(int)(master_vol * scale); if(*left_vol < 0) *left_vol = 0; } /* S_Spatialize */ void S_Spatialize(channel_t *ch){ vec3_t origin; // anything coming from the view entity will always be full volume if(ch->entnum == cl.playernum + 1){ ch->leftvol = ch->master_vol; ch->rightvol = ch->master_vol; return; } if(ch->fixed_origin){ VectorCopy(ch->origin, origin); } else CL_GetEntitySoundOrigin(ch->entnum, origin); S_SpatializeOrigin(origin, ch->master_vol, ch->dist_mult, &ch->leftvol, &ch->rightvol); } /* S_AllocPlaysound */ playsound_t *S_AllocPlaysound(void){ playsound_t *ps; ps = s_freeplays.next; if(ps == &s_freeplays) return NULL; // no free playsounds // unlink from freelist ps->prev->next = ps->next; ps->next->prev = ps->prev; return ps; } /* S_FreePlaysound */ void S_FreePlaysound(playsound_t *ps){ // unlink from channel ps->prev->next = ps->next; ps->next->prev = ps->prev; // add to free list ps->next = s_freeplays.next; s_freeplays.next->prev = ps; ps->prev = &s_freeplays; s_freeplays.next = ps; } /* S_IssuePlaysound Take the next playsound and begin it on the channel This is never called directly by S_Play*, but only by the update loop. */ void S_IssuePlaysound(playsound_t *ps){ channel_t *ch; sfxcache_t *sc; if(s_show->value) Com_Printf("Issue %i\n", ps->begin); // pick a channel to play on ch = S_PickChannel(ps->entnum, ps->entchannel); if(!ch){ S_FreePlaysound(ps); return; } // spatialize if(ps->attenuation == ATTN_STATIC) ch->dist_mult = ps->attenuation * 0.001; else ch->dist_mult = ps->attenuation * 0.0005; ch->master_vol = ps->volume; ch->entnum = ps->entnum; ch->entchannel = ps->entchannel; ch->sfx = ps->sfx; VectorCopy(ps->origin, ch->origin); ch->fixed_origin = ps->fixed_origin; S_Spatialize(ch); ch->pos = 0; sc = S_LoadSound(ch->sfx); ch->end = paintedtime + sc->length; // free the playsound S_FreePlaysound(ps); } struct sfx_s *S_RegisterSexedSound(entity_state_t *ent, char *base){ int n; char *p; struct sfx_s *sfx; char model[MAX_QPATH]; char skin[MAX_QPATH]; char sexedFilename[MAX_QPATH]; char maleFilename[MAX_QPATH]; extern char registered_skins[MAX_MD2SKINS][MAX_QPATH]; extern int registered_skins_index; qboolean skin_registered = false; // determine what model the client is using model[0] = skin[0] = 0; n = CS_PLAYERSKINS + ent->number - 1; if(cl.configstrings[n][0]){ p = strchr(cl.configstrings[n], '\\'); if(p){ p += 1; strcpy(model, p); strcpy(skin, p); p = strchr(model, '/'); if(p) *p = 0; } } // ensure the skin is loaded before we play the wrong sound if(!strcmp(model, "male")){ // dont worry about males skin_registered = true; } else if(model[0] && skin[0]){ // but check everyone else (female, cyborg, ..) for(n = 0; n < registered_skins_index; n++){ if(!strcmp(registered_skins[n], skin)){ skin_registered = true; break; } } } // catch any unregistered skins, theyre being displayed // as males so make them sound like males if(!skin_registered) strcpy(model, "male"); // see if we already know of the model specific sound Com_sprintf(sexedFilename, sizeof(sexedFilename), "#players/%s/%s", model, base + 1); sfx = S_FindName(sexedFilename, false); if(!sfx){ // not yet registered, so try model specific sound sfx = S_RegisterSound(sexedFilename); } if(!sfx){ // that didn't work, so use male, and alias it for future calls Com_sprintf(maleFilename, sizeof(maleFilename), "player/male/%s", base + 1); sfx = S_AliasName(sexedFilename, maleFilename); } return sfx; } /* S_StartSound Validates the parms and ques the sound up if pos is NULL, the sound will be dynamically sourced from the entity Entchannel 0 will never override a playing sound */ void S_StartSound(vec3_t origin, int entnum, int entchannel, sfx_t *sfx, float fvol, float attenuation, float timeofs){ sfxcache_t *sc; int vol; playsound_t *ps, *sort; int start; if(!snd_initialized) return; if(!sfx) return; if(sfx->name[0] == '*') sfx = S_RegisterSexedSound(&cl_entities[entnum].current, sfx->name); // make sure the sound is loaded sc = S_LoadSound(sfx); if(!sc) return; // couldn't load the sound's data vol = fvol * 255; // make the playsound_t ps = S_AllocPlaysound(); if(!ps) return; if(origin){ VectorCopy(origin, ps->origin); ps->fixed_origin = true; } else ps->fixed_origin = false; ps->entnum = entnum; ps->entchannel = entchannel; ps->attenuation = attenuation; ps->volume = vol; ps->sfx = sfx; // drift s_beginofs start = cl.frame.servertime * 0.001 * dma.rate + s_beginofs; if(start < paintedtime){ start = paintedtime; s_beginofs = start -(cl.frame.servertime * 0.001 * dma.rate); } else if(start > paintedtime + 0.3 * dma.rate){ start = paintedtime + 0.1 * dma.rate; s_beginofs = start -(cl.frame.servertime * 0.001 * dma.rate); } else { s_beginofs -= 10; } if(!timeofs) ps->begin = paintedtime; else ps->begin = start + timeofs * dma.rate; // sort into the pending sound list for(sort = s_pendingplays.next; sort != &s_pendingplays && sort->begin < ps->begin; sort = sort->next) ; ps->next = sort; ps->prev = sort->prev; ps->next->prev = ps; ps->prev->next = ps; } /* S_StartLocalSound */ void S_StartLocalSound(char *sound){ sfx_t *sfx; if(!snd_initialized) return; sfx = S_RegisterSound(sound); if(!sfx){ Com_Printf("S_StartLocalSound: can't cache %s\n", sound); return; } S_StartSound(NULL, cl.playernum + 1, 0, sfx, 1, 1, 0); } /* S_ClearBuffer */ void S_ClearBuffer(void){ int clear; if(!snd_initialized) return; s_rawend = 0; if(dma.bits == 8) clear = 0x80; else clear = 0; SND_BeginPainting(); if(dma.buffer){ /* buffer may be read-only, clear manually */ memset(dma.buffer, clear, dma.samples * dma.bits / 8); } SND_Submit(); } /* S_Stop_f */ void S_Stop_f(void){ int i; // clear all the playsounds memset(s_playsounds, 0, sizeof(s_playsounds)); s_freeplays.next = s_freeplays.prev = &s_freeplays; s_pendingplays.next = s_pendingplays.prev = &s_pendingplays; for(i = 0; i < MAX_PLAYSOUNDS; i++){ s_playsounds[i].prev = &s_freeplays; s_playsounds[i].next = s_freeplays.next; s_playsounds[i].prev->next = &s_playsounds[i]; s_playsounds[i].next->prev = &s_playsounds[i]; } // clear all the channels memset(channels, 0, sizeof(channels)); S_ClearBuffer(); } /* S_AddLoopSounds Entities with a ->sound field will generate looped sounds that are automatically started, stopped, and merged together as the entities are sent to the client */ void S_AddLoopSounds(void){ int i, j; int sounds[MAX_EDICTS]; int left, right, left_total, right_total; channel_t *ch; sfx_t *sfx; sfxcache_t *sc; int num; entity_state_t *ent; if(!s_ambient->value) return; for(i = 0; i < cl.frame.num_entities; i++){ num = (cl.frame.parse_entities + i) & (MAX_PARSE_ENTITIES - 1); ent = &cl_parse_entities[num]; sounds[i] = ent->sound; } for(i = 0; i < cl.frame.num_entities; i++){ if(!sounds[i]) continue; sfx = cl.sound_precache[sounds[i]]; if(!sfx) continue; // bad sound effect sc = sfx->cache; if(!sc) continue; num = (cl.frame.parse_entities + i) & (MAX_PARSE_ENTITIES - 1); ent = &cl_parse_entities[num]; // find the total contribution of all sounds of this type S_SpatializeOrigin(ent->origin, 255.0, SOUND_LOOPATTENUATE, &left_total, &right_total); for(j = i + 1; j < cl.frame.num_entities; j++){ if(sounds[j] != sounds[i]) continue; sounds[j] = 0; // don't check this again later num = (cl.frame.parse_entities + j) & (MAX_PARSE_ENTITIES - 1); ent = &cl_parse_entities[num]; S_SpatializeOrigin(ent->origin, 255.0, SOUND_LOOPATTENUATE, &left, &right); left_total += left; right_total += right; } if(left_total == 0 && right_total == 0) continue; // not audible // allocate a channel ch = S_PickChannel(0, 0); if(!ch) return; if(left_total > 255) left_total = 255; if(right_total > 255) right_total = 255; ch->leftvol = left_total; ch->rightvol = right_total; ch->autosound = true; // remove next frame ch->sfx = sfx; /* sometimes, the sc->length argument can become 0, and in that * case we get a SIGFPE in the next modulo. The workaround checks * for this situation and sets the pos and end to zero if true */ if(sc->length == 0){ ch->pos = ch->end = 0; } else { ch->pos = paintedtime % sc->length; ch->end = paintedtime + sc->length - ch->pos; } } } /* S_Update Called once each time through the main loop */ void S_Update(vec3_t origin, vec3_t forward, vec3_t right, vec3_t up){ int i; int total; channel_t *ch; channel_t *combine; if(!snd_initialized) return; if(cls.state != ca_active){ S_Stop_f(); return; } // rebuild scale tables if volume is modified if(s_volume->modified) S_InitScaletable(); VectorCopy(origin, listener_origin); VectorCopy(forward, listener_forward); VectorCopy(right, listener_right); VectorCopy(up, listener_up); combine = NULL; // update spatialization for dynamic sounds ch = channels; for(i = 0; i < MAX_CHANNELS; i++, ch++){ if(!ch->sfx) continue; if(ch->autosound){ // autosounds are regenerated fresh each frame memset(ch, 0, sizeof(*ch)); continue; } S_Spatialize(ch); // respatialize channel if(!ch->leftvol && !ch->rightvol){ memset(ch, 0, sizeof(*ch)); continue; } } // add loopsounds S_AddLoopSounds(); // debugging output if(s_show->value){ total = 0; ch = channels; for(i = 0; i < MAX_CHANNELS; i++, ch++) if(ch->sfx &&(ch->leftvol || ch->rightvol)){ Com_Printf("%3i %3i %s\n", ch->leftvol, ch->rightvol, ch->sfx->name); total++; } Com_Printf("----(%i)---- painted: %i\n", total, paintedtime); } // mix some sound S_Update_(); } void GetSoundtime(void){ int offset; static int buffers; static int oldoffset; int fullsamples; fullsamples = dma.samples / dma.channels; // it is possible to miscount buffers if it has wrapped twice between // calls to S_Update. Oh well. offset = SND_GetDMAPos(); if(offset < oldoffset){ buffers++; // buffer wrapped if(paintedtime > 0x40000000){ // time to chop things off to avoid 32 bit limits buffers = 0; paintedtime = fullsamples; S_Stop_f(); } } oldoffset = offset; soundtime = buffers * fullsamples + offset / dma.channels; } void S_Update_(void){ unsigned endtime; int samps; if(!snd_initialized) return; SND_BeginPainting(); if(!dma.buffer) return; // Updates DMA time GetSoundtime(); // check to make sure that we haven't overshot if(paintedtime < soundtime){ Com_DPrintf("S_Update_ : overflow\n"); paintedtime = soundtime; } // mix ahead of current position endtime = soundtime + s_mixahead->value * dma.rate; // mix to an even submission block size endtime = (endtime + dma.chunk - 1) & ~(dma.chunk - 1); samps = dma.samples >>(dma.channels - 1); if(endtime - soundtime > samps) endtime = soundtime + samps; S_PaintChannels(endtime); SND_Submit(); }