//------------------------------------------------------------------------- /* Copyright (C) 1997, 2005 - 3D Realms Entertainment Copyright (C) 2019 Christoph Oelckers This file is part of Shadow Warrior version 1.2 Shadow Warrior 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Original Source: 1997 - Frank Maddin and Jim Norwood Prepared for public release: 03/28/2005 - Charlie Wiederhold, 3D Realms */ //------------------------------------------------------------------------- #include "ns.h" #include "compat.h" #include "build.h" #include "keys.h" #include "names2.h" #include "mytypes.h" #include "gamedefs.h" #include "config.h" #include "panel.h" #include "game.h" #include "sounds.h" #include "ai.h" #include "network.h" #include "cache.h" #include "text.h" #include "rts.h" #include "menus.h" #include "config.h" #include "menu/menu.h" #include "raze_music.h" #include "raze_sound.h" #include "filesystem.h" #include "serializer.h" BEGIN_SW_NS enum EChanExFlags { CHANEXF_NODOPPLER = 0x20000000, CHANEXF_DONTPAN = 0x40000000, }; // Parentally locked sounds list int PLocked_Sounds[] = { 483,328,334,335,338,478,450,454,452,453,456,457,458,455,460,462, 461,464,466,465,467,463,342,371,254,347,350,432,488,489,490,76,339, 499,500,506,479,480,481,482,78,600,467,548,547,544,546,545,542,541,540, 539,536,529,525,522,521,515,516,612,611,589,625,570,569,567,565, 558,557 }; // // Includes digi.h to build the table // #define DIGI_TABLE VOC_INFO voc[] = { #include "digi.h" }; #undef DIGI_TABLE // // Includes ambient.h to build the table of ambient sounds for game // #define AMBIENT_TABLE AMB_INFO ambarray[] = { #include "ambient.h" }; #undef AMBIENT_TABLE #define MAX_AMBIENT_SOUNDS 82 //========================================================================== // // // //========================================================================== float S_ConvertPitch(int lpitch) { return pow(2, lpitch / 1200.); } //========================================================================== // // Sound Distance Calculation // //========================================================================== enum { MAXLEVLDIST = 19000, // The higher the number, the further away you can hear sound DECAY_CONST = 4000 }; short SoundDist(int x, int y, int z, int basedist) { double tx, ty, tz; double sqrdist, retval; extern short screenpeek; tx = fabs(Player[screenpeek].posx - x); ty = fabs(Player[screenpeek].posy - y); tz = fabs((Player[screenpeek].posz - z) >> 4); // Use the Pythagreon Theorem to compute the magnitude of a 3D vector sqrdist = fabs(tx * tx + ty * ty + tz * tz); retval = sqrt(sqrdist); if (basedist < 0) // if basedist is negative { double decayshift = 2; int decay = labs(basedist) / DECAY_CONST; for (int i = 0; i < decay; i++) decayshift *= 2; if (fabs(double(basedist) / decayshift) >= retval) retval = 0; else retval *= decay; } else { if (basedist > retval) retval = 0; else retval -= basedist; } retval = retval * 256 / MAXLEVLDIST; if (retval < 0) retval = 0; if (retval > 255) retval = 255; return retval; } //========================================================================== // // Calculate rolloff info. // //========================================================================== FRolloffInfo GetRolloff(int basedist) { FRolloffInfo info; if (basedist < 0) // if basedist is negative { double decayshift = 2; int decay = labs(basedist) / DECAY_CONST; for (int i = 0; i < decay; i++) decayshift *= 2; info.RolloffType = ROLLOFF_Doom; info.MinDistance = (float)(-basedist / decayshift / 16.); info.MaxDistance = MAXLEVLDIST / 16.f / decay; } else { info.RolloffType = ROLLOFF_Doom; info.MinDistance = basedist / 16.f; info.MaxDistance = info.MinDistance + MAXLEVLDIST / 16.f; } return info; } //========================================================================== // // // Ambient sounds // // //========================================================================== struct AmbientSound { SPRITEp sp; int ambIndex; int vocIndex; int ChanFlags; int maxIndex; int curIndex; bool intermit; }; static TArray ambients; //========================================================================== // // // //========================================================================== void StopAmbientSound(void) { for (auto amb : ambients) { soundEngine->StopSound(SOURCE_Ambient, amb, -1); delete amb; } ambients.Clear(); } //========================================================================== // // Play a sound // //========================================================================== void InitAmbient(int num, SPRITEp sp) { VOC_INFOp vp; int pitch = 0; short angle, sound_dist; int tx, ty, tz; uint8_t priority; int maxtics = 0; if (!snd_ambience) return; // Ambient sounds need special treatment if (num < 0 || num > MAX_AMBIENT_SOUNDS) { sprintf(ds, "Invalid or out of range ambient sound number %d\n", num); PutStringInfo(Player + screenpeek, ds); return; } auto vnum = ambarray[num].diginame; if (!soundEngine->isValidSoundId(vnum)) { return; // linked sound does not exist. } auto amb = new AmbientSound; amb->sp = sp; amb->ambIndex = num; amb->vocIndex = vnum; amb->ChanFlags = CHANF_TRANSIENT; if (ambarray[num].ambient_flags & v3df_dontpan) amb->ChanFlags |= EChanFlags::FromInt(CHANEXF_DONTPAN); if (voc[vnum].voc_flags & vf_loop) amb->ChanFlags |= CHANF_LOOP; amb->maxIndex = ambarray[num].maxtics; amb->curIndex = 0; amb->intermit = !!(ambarray[num].ambient_flags & v3df_intermit); ambients.Push(amb); } //========================================================================== // // // //========================================================================== void StartAmbientSound(void) { short i, nexti; extern SWBOOL InMenuLevel; if (InMenuLevel || !SoundEnabled()) return; // Don't restart ambience if no level is active! Will crash game. TRAVERSE_SPRITE_STAT(headspritestat[STAT_AMBIENT], i, nexti) { SPRITEp sp = &sprite[i]; InitAmbient(sp->lotag, sp); } } //========================================================================== // // // //========================================================================== static void RestartAmbient(AmbientSound* amb) { auto& vp = voc[amb->vocIndex]; auto rolloff = GetRolloff(vp.voc_distance); int pitch = 0; if (vp.pitch_hi <= vp.pitch_lo) pitch = vp.pitch_lo; else pitch = vp.pitch_lo + (STD_RANDOM_RANGE(vp.pitch_hi - vp.pitch_lo)); amb->curIndex = (int)totalclock; if (!soundEngine->IsSourcePlayingSomething(SOURCE_Ambient, amb, CHAN_BODY, amb->vocIndex)) soundEngine->StartSound(SOURCE_Ambient, amb, nullptr, CHAN_BODY, EChanFlags::FromInt(amb->ChanFlags), amb->vocIndex, 1.f, ATTN_NORM, &rolloff, S_ConvertPitch(pitch)); } //========================================================================== // // // //========================================================================== static int RandomizeAmbientSpecials(int handle) { #define MAXRNDAMB 12 static int ambrand[] = { 56,57,58,59,60,61,62,63,64,65,66,67 }; int i; // If ambient sound is found in the array, randomly pick a new sound for (i = 0; i < MAXRNDAMB; i++) { if (handle == ambrand[i]) return ambrand[STD_RANDOM_RANGE(MAXRNDAMB - 1)]; } return -1; } //========================================================================== // // // //========================================================================== static void DoTimedSound(AmbientSound* amb) { if ((int)totalclock >= amb->curIndex + amb->maxIndex) { if (!soundEngine->IsSourcePlayingSomething(SOURCE_Ambient, amb, CHAN_BODY)) { // Check for special case ambient sounds. Since the sound is stopped and doesn't occupy a real channel at this time we can just swap out the sound ID before restarting it. int ambid = RandomizeAmbientSpecials(amb->ambIndex); if (ambid != -1) { amb->vocIndex = ambarray[ambid].diginame; amb->maxIndex = STD_RANDOM_RANGE(ambarray[ambid].maxtics); } RestartAmbient(amb); } } } //========================================================================== // // // //========================================================================== static void UpdateAmbients() { for (auto& amb : ambients) { auto sp = amb->sp; auto sdist = SoundDist(sp->pos.x, sp->pos.y, sp->pos.z, voc[amb->vocIndex].voc_distance); if (sdist < 255 && amb->vocIndex == DIGI_WHIPME) { PLAYERp pp = Player + screenpeek; if (!FAFcansee(sp->pos.x, sp->pos.y, sp->pos.z, sp->sectnum, pp->posx, pp->posy, pp->posz, pp->cursectnum)) { sdist = 255; } } if (sdist < 255) { if (amb->intermit) DoTimedSound(amb); else RestartAmbient(amb); } else { soundEngine->StopSound(SOURCE_Ambient, amb, -1); } } } //========================================================================== // // end of ambient sounds // //========================================================================== //========================================================================== // // // //========================================================================== class SWSoundEngine : public SoundEngine { // client specific parts of the sound engine go in this class. void CalcPosVel(int type, const void* source, const float pt[3], int channum, int chanflags, FSoundID chanSound, FVector3* pos, FVector3* vel, FSoundChan* chan) override; TArray ReadSound(int lumpnum) override; public: SWSoundEngine() { S_Rolloff.RolloffType = ROLLOFF_Doom; S_Rolloff.MinDistance = 0; // These are the default values, SW uses a few different rolloff settings. S_Rolloff.MaxDistance = 1187; } int SoundSourceIndex(FSoundChan* chan) override { if (chan->SourceType == SOURCE_Player) return int(PLAYERp(chan->Source) - Player); if (chan->SourceType == SOURCE_Unattached && chan->Source) return int(SPRITEp(chan->Source) - sprite); return 0; } void SetSource(FSoundChan* chan, int index) override { if (chan->SourceType == SOURCE_Player) { if (index < 0 || index >= MAX_SW_PLAYERS_REG) index = 0; chan->Source = &Player[index]; } else if (chan->SourceType == SOURCE_Unattached && index >= 0) { chan->Source = &sprite[index]; } else chan->Source = nullptr; } void StopChannel(FSoundChan* chan) override { if (chan && chan->SysChannel != NULL && !(chan->ChanFlags & CHANF_EVICTED) && chan->SourceType == SOURCE_Actor) { chan->Source = NULL; chan->SourceType = SOURCE_Unattached; } SoundEngine::StopChannel(chan); } }; //========================================================================== // // // //========================================================================== TArray SWSoundEngine::ReadSound(int lumpnum) { auto wlump = fileSystem.OpenFileReader(lumpnum); return wlump.Read(); } //========================================================================== // // // //========================================================================== void InitFX(void) { if (soundEngine) return; // just in case. soundEngine = new SWSoundEngine; auto &S_sfx = soundEngine->GetSounds(); S_sfx.Resize(countof(voc)); for (auto& sfx : S_sfx) { sfx.Clear(); sfx.lumpnum = sfx_empty; } for (size_t i = 1; i < countof(voc); i++) { auto& entry = voc[i]; auto lump = S_LookupSound(entry.name); if (lump > 0) { auto& newsfx = S_sfx[i]; newsfx.name = entry.name; newsfx.lumpnum = lump; newsfx.NearLimit = 6; newsfx.bTentative = false; } } soundEngine->HashSounds(); for (auto& sfx : S_sfx) { soundEngine->CacheSound(&sfx); } } //========================================================================== // // // //========================================================================== void SWSoundEngine::CalcPosVel(int type, const void* source, const float pt[3], int channum, int chanflags, FSoundID chanSound, FVector3* pos, FVector3* vel, FSoundChan* chan) { if (pos != nullptr) { PLAYERp pp = Player + screenpeek; FVector3 campos = GetSoundPos((vec3_t*)pp); vec3_t *vpos = nullptr; if (vel) vel->Zero(); if (type == SOURCE_Unattached) { pos->X = pt[0]; pos->Y = pt[1]; pos->Z = pt[2]; } else if (type == SOURCE_Actor || type == SOURCE_Player) { vpos = type == SOURCE_Actor ? &((SPRITEp)source)->pos : (vec3_t*)&((PLAYERp)source)->posx; FVector3 npos = GetSoundPos(vpos); *pos = npos; if (!(chanflags & CHANEXF_NODOPPLER) && vel) { // Hack alert. Velocity may only be set if a) the sound is already running and b) an actual sound channel is modified. // It remains to be seen if this is actually workable. I have my doubts. The velocity should be taken from a stable source. if (chan && !(chanflags & (CHANF_JUSTSTARTED | CHANF_EVICTED))) { *vel = (npos - FVector3(pt[0], pt[1], pt[2])) * 40; // SW ticks 40 times a second. chan->Point[0] = npos.X; chan->Point[1] = npos.Y; chan->Point[2] = npos.Z; } } } else if (type == SOURCE_Ambient) { auto sp = ((AmbientSound*)source)->sp; vec3_t* vpos = &sp->pos; FVector3 npos = GetSoundPos(vpos); // Can the ambient sound see the player? If not, tone it down some. if ((chanflags & CHANF_LOOP)) { if (!FAFcansee(vpos->x, vpos->y, vpos->z, sp->sectnum, pp->posx, pp->posy, pp->posz, pp->cursectnum)) { auto distvec = npos - campos; npos = campos + distvec * 1.75f; // Play more quietly } } *pos = npos; } if (vpos && chanflags & CHANEXF_DONTPAN) { // For unpanned sounds the volume must be set directly and the position taken from the listener. *pos = campos; auto sdist = SoundDist(vpos->x, vpos->y, vpos->z, voc[chanSound].voc_distance); if (chan) SetVolume(chan, (255 - sdist) * (1 / 255.f)); } if ((chanflags & CHANF_LISTENERZ) && type != SOURCE_None) { pos->Y = campos.Y; } } } //========================================================================== // // Main function to update 3D sound array // //========================================================================== void DoUpdateSounds(void) { PLAYERp pp = Player + screenpeek; SoundListener listener; listener.angle = -fix16_to_float(pp->q16ang) * pi::pi() / 1024; // Build uses a period of 2048. listener.velocity.Zero(); listener.position = GetSoundPos((vec3_t*)&pp->posx); listener.underwater = false; // This should probably use a real environment instead of the pitch hacking in S_PlaySound3D. // listenactor->waterlevel == 3; //assert(primaryLevel->Zones.Size() > listenactor->Sector->ZoneNumber); listener.Environment = 0;// primaryLevel->Zones[listenactor->Sector->ZoneNumber].Environment; listener.valid = true; listener.ListenerObject = pp; soundEngine->SetListener(listener); UpdateAmbients(); soundEngine->UpdateSounds((int)totalclock); } //========================================================================== // // Play a sound // //========================================================================== int _PlaySound(int num, SPRITEp sp, PLAYERp pp, vec3_t* pos, Voc3D_Flags flags, int channel, EChanFlags cflags) { if (Prediction || !SoundEnabled() || !soundEngine->isValidSoundId(num)) return -1; SPRITEp sps = sp; // Weed out parental lock sounds if PLock is active if (adult_lockout || Global_PLock) { for (unsigned i = 0; i < sizeof(PLocked_Sounds); i++) { if (num == PLocked_Sounds[i]) return -1; } } auto vp = &voc[num]; int sourcetype = SOURCE_None; cflags |= channel == 8 ? CHANF_OVERLAP : CHANF_NONE; // for the default channel we do not want to have sounds stopping each other. void* source = nullptr; // If the sound is not supposd to be positioned, it may not be linked to the launching actor. if (!(flags & v3df_follow)) { if (sp && !pos) { pos = &sp->pos; sp = nullptr; } else if (pp && !pos) { pos = (vec3_t*)&pp->posx; pp = nullptr; } } if (pos != nullptr) { sourcetype = SOURCE_Unattached; } else if (sp != nullptr) { source = sp; sourcetype = SOURCE_Actor; } else if (pp != nullptr) { source = pp; sourcetype = SOURCE_Player; } // Otherwise it's an unpositioned sound. if (flags & v3df_doppler) cflags |= EChanFlags::FromInt(CHANEXF_NODOPPLER); // this must ensure that CalcPosVel always zeros the velocity. if (flags & v3df_dontpan) cflags |= EChanFlags::FromInt(CHANEXF_DONTPAN); // beware of hackery to emulate this. if (vp->voc_flags & vf_loop) cflags |= CHANF_LOOP; // with the new sound engine these can just be started and don't have to be stopped ever. int pitch = 0; if (vp->pitch_hi <= vp->pitch_lo) pitch = vp->pitch_lo; else if (vp->pitch_hi != vp->pitch_lo) pitch = vp->pitch_lo + (STD_RANDOM_RANGE(vp->pitch_hi - vp->pitch_lo)); auto rolloff = GetRolloff(vp->voc_distance); FVector3 spos = pos ? GetSoundPos(pos) : FVector3(0, 0, 0); auto chan = soundEngine->StartSound(sourcetype, source, &spos, channel, cflags, num, 1.f, ATTN_NORM, &rolloff, S_ConvertPitch(pitch)); if (chan && sourcetype == SOURCE_Unattached) chan->Source = sps; // needed for sound termination. return 1; } //========================================================================== // // // //========================================================================== void PlaySoundRTS(int rts_num) { if (!adult_lockout && SoundEnabled() && RTS_IsInitialized() && snd_speech) { auto sid = RTS_GetSoundID(rts_num - 1); if (sid != -1) { soundEngine->StartSound(SOURCE_Unattached, nullptr, nullptr, CHAN_VOICE, 0, sid, 0.8f, ATTN_NONE); } } } //========================================================================== // // // //========================================================================== void COVER_SetReverb(int amt) { FX_SetReverb(amt); } //========================================================================== // // Deletes vocs in the 3D sound queue with no owners // //========================================================================== void DeleteNoSoundOwner(short spritenum) { if (!soundEngine) return; SPRITEp sp = &sprite[spritenum]; soundEngine->EnumerateChannels([=](FSoundChan* chan) { if (chan->Source == sp && chan->ChanFlags & CHANF_LOOP) { soundEngine->StopChannel(chan); } return false; }); } //========================================================================== // // This is called from KillSprite to kill a follow sound with no valid sprite owner // Stops any active sound with the follow bit set, even play once sounds. // //========================================================================== void DeleteNoFollowSoundOwner(short spritenum) { SPRITEp sp = &sprite[spritenum]; soundEngine->StopSound(SOURCE_Actor, sp, -1); // all non-follow sounds are SOURCE_Unattached } //========================================================================== // // Terminate the sounds list // //========================================================================== void Terminate3DSounds(void) { soundEngine->EnumerateChannels([](FSoundChan* chan) { if (chan->SourceType == SOURCE_Actor || chan->SourceType == SOURCE_Unattached || (chan->SourceType == SOURCE_Player && chan->EntChannel != CHAN_VOICE)) { soundEngine->StopChannel(chan); } return false; }); } //========================================================================== // // no longer needed, only left to avoid changing the game code // //========================================================================== void Set3DSoundOwner(short spritenum) { } //========================================================================== // // Play a sound using special sprite setup // //========================================================================== void PlaySpriteSound(short spritenum, int attrib_ndx, Voc3D_Flags flags) { SPRITEp sp = &sprite[spritenum]; USERp u = User[spritenum]; ASSERT(u); PlaySound(u->Attrib->Sounds[attrib_ndx], sp, flags); } //========================================================================== // // // //========================================================================== int _PlayerSound(int num, PLAYERp pp) { int handle; VOC_INFOp vp; if (Prediction) return 0; if (pp < Player || pp >= Player + MAX_SW_PLAYERS) { return 0; } if (num < 0 || num >= DIGI_MAX || !soundEngine->isValidSoundId(num)) return 0; if (TEST(pp->Flags, PF_DEAD)) return 0; // You're dead, no talking! // If this is a player voice and he's already yacking, forget it. vp = &voc[num]; // Not a player voice, bail. if (vp->priority != PRI_PLAYERVOICE && vp->priority != PRI_PLAYERDEATH) return 0; // The surfacing sound should not block other player speech. if (soundEngine->IsSourcePlayingSomething(SOURCE_Player, pp, CHAN_VOICE, DIGI_SURFACE)) { soundEngine->StopSound(SOURCE_Player, pp, CHAN_VOICE); } // He wasn't talking, but he will be now. if (!soundEngine->IsSourcePlayingSomething(SOURCE_Player, pp, CHAN_VOICE)) { soundEngine->StartSound(SOURCE_Player, pp, nullptr, CHAN_VOICE, 0, num, 1.f, ATTN_NORM); } return 0; } void StopPlayerSound(PLAYERp pp, int which) { soundEngine->StopSound(SOURCE_Player, pp, CHAN_VOICE, which); } bool SoundValidAndActive(SPRITEp spr, int channel) { return soundEngine->IsSourcePlayingSomething(SOURCE_Actor, spr, channel); } /* ============================================================================ = = High level sound code (not directly engine related) = ============================================================================ */ int PlayerPainVocs[] = { DIGI_PLAYERPAIN1, DIGI_PLAYERPAIN2, DIGI_PLAYERPAIN3, DIGI_PLAYERPAIN4, DIGI_PLAYERPAIN5 }; // Don't have these sounds yet int PlayerLowHealthPainVocs[] = { DIGI_HURTBAD1, DIGI_HURTBAD2, DIGI_HURTBAD3, DIGI_HURTBAD4, DIGI_HURTBAD5 }; int TauntAIVocs[] = { DIGI_TAUNTAI1, DIGI_TAUNTAI2, DIGI_TAUNTAI3, DIGI_TAUNTAI4, DIGI_TAUNTAI5, DIGI_TAUNTAI6, DIGI_TAUNTAI7, DIGI_TAUNTAI8, DIGI_TAUNTAI9, DIGI_TAUNTAI10, DIGI_COWABUNGA, DIGI_NOCHARADE, DIGI_TIMETODIE, DIGI_EATTHIS, DIGI_FIRECRACKERUPASS, DIGI_HOLYCOW, DIGI_HAHA2, DIGI_HOLYPEICESOFCOW, DIGI_HOLYSHIT, DIGI_HOLYPEICESOFSHIT, DIGI_PAYINGATTENTION, DIGI_EVERYBODYDEAD, DIGI_KUNGFU, DIGI_HOWYOULIKEMOVE, DIGI_HAHA3, DIGI_NOMESSWITHWANG, DIGI_RAWREVENGE, DIGI_YOULOOKSTUPID, DIGI_TINYDICK, DIGI_NOTOURNAMENT, DIGI_WHOWANTSWANG, DIGI_MOVELIKEYAK, DIGI_ALLINREFLEXES }; int PlayerGetItemVocs[] = { DIGI_GOTITEM1, DIGI_HAHA1, DIGI_BANZAI, DIGI_COWABUNGA, DIGI_TIMETODIE }; int PlayerYellVocs[] = { DIGI_PLAYERYELL1, DIGI_PLAYERYELL2, DIGI_PLAYERYELL3 }; //========================================================================== // // PLays music // //========================================================================== extern short Level; SWBOOL PlaySong(const char* mapname, const char* song_file_name, int cdaudio_track, bool isThemeTrack) //(nullptr, nullptr, -1, false) starts the normal level music. { // Play CD audio if enabled. if (cdaudio_track >= 0 && (mus_redbook || *song_file_name == 0)) { FStringf trackname("track%02d.ogg", cdaudio_track); if (!Mus_Play(mapname, trackname, true)) { Printf("Can't find CD track %i!\n", cdaudio_track); } else return true; } if (!Mus_Play(mapname, song_file_name, true)) { // try the CD track anyway if no MIDI could be found (the original game doesn't have any MIDI, it was CD Audio only, this avoids no music playing if mus_redbook is off.) FStringf trackname("track%02d.ogg", cdaudio_track); if (!Mus_Play(nullptr, trackname, true)) return false; } return true; } void StopSound(void) { // This gets also called on shutdown. StopAmbientSound(); if (soundEngine) soundEngine->StopAllChannels(); Mus_Stop(); } void StopFX() { StopAmbientSound(); if (soundEngine) soundEngine->StopAllChannels(); } END_SW_NS