/* ** s_sound.cpp ** Main sound engine ** **--------------------------------------------------------------------------- ** Copyright 1998-2016 Randy Heit ** Copyright 2002-2019 Christoph Oelckers ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** */ #include #include #include "s_soundinternal.h" #include "m_swap.h" #include "superfasthash.h" #include "c_cvars.h" #include "name.h" #include "filesystem.h" #include "cmdlib.h" #include "gamecontrol.h" #include "serializer.h" #include "build.h" enum { DEFAULT_PITCH = 128, }; SoundEngine* soundEngine; int sfx_empty = -1; //========================================================================== // // S_Init // //========================================================================== void SoundEngine::Init(TArray &curve, int factor) { StopAllChannels(); // Free all channels for use. while (Channels != NULL) { ReturnChannel(Channels); } S_SoundCurve = std::move(curve); SndCurveFactor = (uint8_t)factor; } //========================================================================== // // SoundEngine::Clear // //========================================================================== void SoundEngine::Clear() { StopAllChannels(); UnloadAllSounds(); GetSounds().Clear(); ClearRandoms(); } //========================================================================== // // S_Shutdown // //========================================================================== void SoundEngine::Shutdown () { FSoundChan *chan, *next; StopAllChannels(); for (chan = FreeChannels; chan != NULL; chan = next) { next = chan->NextChan; delete chan; } FreeChannels = NULL; } //========================================================================== // // MarkUsed // //========================================================================== void SoundEngine::MarkUsed(int id) { if ((unsigned)id < S_sfx.Size()) { S_sfx[id].bUsed = true; } } //========================================================================== // // Cache all marked sounds // //========================================================================== void SoundEngine::CacheMarkedSounds() { // Don't unload sounds that are playing right now. for (FSoundChan* chan = Channels; chan != nullptr; chan = chan->NextChan) { MarkUsed(chan->SoundID); } for (unsigned i = 1; i < S_sfx.Size(); ++i) { if (S_sfx[i].bUsed) { CacheSound(&S_sfx[i]); } } for (unsigned i = 1; i < S_sfx.Size(); ++i) { if (!S_sfx[i].bUsed && S_sfx[i].link == sfxinfo_t::NO_LINK) { UnloadSound(&S_sfx[i]); } } } //========================================================================== // // S_CacheSound // //========================================================================== void SoundEngine::CacheSound (sfxinfo_t *sfx) { if (GSnd && !sfx->bTentative) { sfxinfo_t *orig = sfx; while (!sfx->bRandomHeader && sfx->link != sfxinfo_t::NO_LINK) { sfx = &S_sfx[sfx->link]; } if (sfx->bRandomHeader) { CacheRandomSound(sfx); } else { // Since we do not know in what format the sound will be used, we have to cache both. LoadSound(sfx); sfx->bUsed = true; } } } //========================================================================== // // S_UnloadSound // //========================================================================== void SoundEngine::UnloadSound (sfxinfo_t *sfx) { if (sfx->data.isValid()) GSnd->UnloadSound(sfx->data); sfx->data.Clear(); } //========================================================================== // // S_GetChannel // // Returns a free channel for the system sound interface. // //========================================================================== FSoundChan *SoundEngine::GetChannel(void *syschan) { FSoundChan *chan; if (FreeChannels != NULL) { chan = FreeChannels; UnlinkChannel(chan); } else { chan = new FSoundChan; memset(chan, 0, sizeof(*chan)); } LinkChannel(chan, &Channels); chan->SysChannel = syschan; return chan; } //========================================================================== // // S_ReturnChannel // // Returns a channel to the free pool. // //========================================================================== void SoundEngine::ReturnChannel(FSoundChan *chan) { UnlinkChannel(chan); memset(chan, 0, sizeof(*chan)); LinkChannel(chan, &FreeChannels); } //========================================================================== // // S_UnlinkChannel // //========================================================================== void SoundEngine::UnlinkChannel(FSoundChan *chan) { *(chan->PrevChan) = chan->NextChan; if (chan->NextChan != NULL) { chan->NextChan->PrevChan = chan->PrevChan; } } //========================================================================== // // S_LinkChannel // //========================================================================== void SoundEngine::LinkChannel(FSoundChan *chan, FSoundChan **head) { chan->NextChan = *head; if (chan->NextChan != NULL) { chan->NextChan->PrevChan = &chan->NextChan; } *head = chan; chan->PrevChan = head; } //========================================================================== // // // //========================================================================== TArray SoundEngine::AllActiveChannels() { TArray chans; for (auto chan = Channels; chan != nullptr; chan = chan->NextChan) { // If the sound is forgettable, this is as good a time as // any to forget about it. And if it's a UI sound, it shouldn't // be stored in the savegame. if (!(chan->ChanFlags & (CHANF_FORGETTABLE | CHANF_UI | CHANF_TRANSIENT))) { chans.Push(chan); } } return chans; } //========================================================================== // // // //========================================================================== FString SoundEngine::ListSoundChannels() { FString output; FSoundChan* chan; int count = 0; for (chan = Channels; chan != nullptr; chan = chan->NextChan) { if (!(chan->ChanFlags & CHANF_EVICTED)) { FVector3 chanorigin; CalcPosVel(chan, &chanorigin, nullptr); output.AppendFormat("%s at (%1.5f, %1.5f, %1.5f)\n", (const char*)S_sfx[chan->SoundID].name.GetChars(), chanorigin.X, chanorigin.Y, chanorigin.Z); count++; } } output.AppendFormat("%d sounds playing\n", count); return output; } // [RH] Split S_StartSoundAtVolume into multiple parts so that sounds can // be specified both by id and by name. Also borrowed some stuff from // Hexen and parameters from Quake. //========================================================================== // // CalcPosVel // // Retrieves a sound's position and velocity for 3D sounds. This version // is for an already playing sound. // //========================================================================= void SoundEngine::CalcPosVel(FSoundChan *chan, FVector3 *pos, FVector3 *vel) { CalcPosVel(chan->SourceType, chan->Source, chan->Point, chan->EntChannel, chan->ChanFlags, chan->OrgID, pos, vel, chan); } bool SoundEngine::ValidatePosVel(const FSoundChan* const chan, const FVector3& pos, const FVector3& vel) { return ValidatePosVel(chan->SourceType, chan->Source, pos, vel); } //========================================================================== // // // //========================================================================== FSoundID SoundEngine::ResolveSound(const void *, int, FSoundID soundid, float &attenuation) { const sfxinfo_t &sfx = S_sfx[soundid]; if (sfx.bRandomHeader) { // Random sounds attenuate based on the original (random) sound as well as the chosen one. attenuation *= sfx.Attenuation; return PickReplacement (soundid); } else { return sfx.link; } } //========================================================================== // // S_StartSound // // 0 attenuation means full volume over whole primaryLevel-> // 0 < attenuation means to scale the distance by that amount when // calculating volume. // //========================================================================== FSoundChan *SoundEngine::StartSound(int type, const void *source, const FVector3 *pt, int channel, EChanFlags flags, FSoundID sound_id, float volume, float attenuation, FRolloffInfo *forcedrolloff, float spitch) { sfxinfo_t *sfx; EChanFlags chanflags = flags; int basepriority; int org_id; int pitch; FSoundChan *chan; FVector3 pos, vel; FRolloffInfo *rolloff; if (sound_id <= 0 || volume <= 0 || userConfig.nosound ) return NULL; // prevent crashes. if (type == SOURCE_Unattached && pt == nullptr) type = SOURCE_None; org_id = sound_id; CalcPosVel(type, source, &pt->X, channel, chanflags, sound_id, &pos, &vel, nullptr); if (!ValidatePosVel(type, source, pos, vel)) { return nullptr; } sfx = &S_sfx[sound_id]; // Scale volume according to SNDINFO data. volume = std::min(volume * sfx->Volume, 1.f); if (volume <= 0) return NULL; // When resolving a link we do not want to get the NearLimit of // the referenced sound so some additional checks are required int near_limit = sfx->NearLimit; float limit_range = sfx->LimitRange; auto pitchmask = sfx->PitchMask; rolloff = &sfx->Rolloff; // Resolve player sounds, random sounds, and aliases while (sfx->link != sfxinfo_t::NO_LINK) { sound_id = ResolveSound(source, type, sound_id, attenuation); if (sound_id < 0) return nullptr; auto newsfx = &S_sfx[sound_id]; if (newsfx != sfx) { if (near_limit < 0) { near_limit = newsfx->NearLimit; limit_range = newsfx->LimitRange; } if (rolloff->MinDistance == 0) { rolloff = &newsfx->Rolloff; } sfx = newsfx; } else return nullptr; // nothing got replaced, prevent an endless loop, } // Attenuate the attenuation based on the sound. attenuation *= sfx->Attenuation; // The passed rolloff overrides any sound-specific rolloff. if (forcedrolloff != NULL && forcedrolloff->MinDistance != 0) { rolloff = forcedrolloff; } // If no valid rolloff was set, use the global default. if (rolloff->MinDistance == 0) { rolloff = &S_Rolloff; } // If this is a singular sound, don't play it if it's already playing. if (sfx->bSingular && CheckSingular(sound_id)) { chanflags |= CHANF_EVICTED; } // If the sound is unpositioned or comes from the listener, it is // never limited. if (type == SOURCE_None || source == listener.ListenerObject) { near_limit = 0; } // If this sound doesn't like playing near itself, don't play it if // that's what would happen. (Does this really need the SOURCE_Actor restriction?) if (near_limit > 0 && CheckSoundLimit(sfx, pos, near_limit, limit_range, type, type == SOURCE_Actor? source : nullptr, channel)) { chanflags |= CHANF_EVICTED; } // If the sound is blocked and not looped, return now. If the sound // is blocked and looped, pretend to play it so that it can // eventually play for real. if ((chanflags & (CHANF_EVICTED | CHANF_LOOP)) == CHANF_EVICTED) { return NULL; } // Make sure the sound is loaded. sfx = LoadSound(sfx); // The empty sound never plays. if (sfx->lumpnum == sfx_empty) { return NULL; } // Select priority. if (type == SOURCE_None || source == listener.ListenerObject) { basepriority = 80; } else { basepriority = 0; } int seen = 0; if (source != NULL && channel == CHAN_AUTO) { // Select a channel that isn't already playing something. // Try channel 0 first, then travel from channel 7 down. if (!IsChannelUsed(type, source, 0, &seen)) { channel = 0; } else { for (channel = 7; channel > 0; --channel) { if (!IsChannelUsed(type, source, channel, &seen)) { break; } } if (channel == 0) { // Crap. No free channels. return NULL; } } } // If this actor is already playing something on the selected channel, stop it. if (!(chanflags & CHANF_OVERLAP) && type != SOURCE_None && ((source == NULL && channel != CHAN_AUTO) || (source != NULL && IsChannelUsed(type, source, channel, &seen)))) { for (chan = Channels; chan != NULL; chan = chan->NextChan) { if (chan->SourceType == type && chan->EntChannel == channel) { const bool foundit = (type == SOURCE_Unattached) ? (chan->Point[0] == pt->X && chan->Point[2] == pt->Z && chan->Point[1] == pt->Y) : (chan->Source == source); if (foundit) { StopChannel(chan); } } } } // sound is paused and a non-looped sound is being started. // Such a sound would play right after unpausing which wouldn't sound right. if (!(chanflags & CHANF_LOOP) && !(chanflags & (CHANF_UI|CHANF_NOPAUSE)) && SoundPaused) { return NULL; } // Vary the sfx pitches. if (pitchmask != 0) { pitch = DEFAULT_PITCH - (rand() & pitchmask) + (rand() & pitchmask); } else { pitch = DEFAULT_PITCH; } if (chanflags & CHANF_EVICTED) { chan = NULL; } else { int startflags = 0; if (chanflags & CHANF_LOOP) startflags |= SNDF_LOOP; if (chanflags & CHANF_AREA) startflags |= SNDF_AREA; if (chanflags & (CHANF_UI|CHANF_NOPAUSE)) startflags |= SNDF_NOPAUSE; if (chanflags & CHANF_UI) startflags |= SNDF_NOREVERB; if (attenuation > 0 && type != SOURCE_None) { chan = (FSoundChan*)GSnd->StartSound3D (sfx->data, &listener, float(volume), rolloff, float(attenuation), pitch, basepriority, pos, vel, channel, startflags, NULL); } else { chan = (FSoundChan*)GSnd->StartSound (sfx->data, float(volume), pitch, startflags, NULL); } } if (chan == NULL && (chanflags & CHANF_LOOP)) { chan = (FSoundChan*)GetChannel(NULL); GSnd->MarkStartTime(chan); chanflags |= CHANF_EVICTED; } if (attenuation > 0 && type != SOURCE_None) { chanflags |= CHANF_IS3D | CHANF_JUSTSTARTED; } else { chanflags |= CHANF_LISTENERZ | CHANF_JUSTSTARTED; } if (chan != NULL) { chan->SoundID = sound_id; chan->OrgID = FSoundID(org_id); chan->EntChannel = channel; chan->Volume = float(volume); chan->ChanFlags |= chanflags; chan->NearLimit = near_limit; chan->LimitRange = limit_range; chan->Pitch = pitch; chan->Priority = basepriority; chan->DistanceScale = float(attenuation); chan->SourceType = type; if (type == SOURCE_Unattached) { chan->Point[0] = pt->X; chan->Point[1] = pt->Y; chan->Point[2] = pt->Z; } else if (type != SOURCE_None) { chan->Source = source; } if (spitch > 0.0) SetPitch(chan, spitch); } return chan; } //========================================================================== // // S_RestartSound // // Attempts to restart looping sounds that were evicted from their channels. // //========================================================================== void SoundEngine::RestartChannel(FSoundChan *chan) { assert(chan->ChanFlags & CHANF_EVICTED); FSoundChan *ochan; sfxinfo_t *sfx = &S_sfx[chan->SoundID]; // If this is a singular sound, don't play it if it's already playing. if (sfx->bSingular && CheckSingular(chan->SoundID)) return; sfx = LoadSound(sfx); // The empty sound never plays. if (sfx->lumpnum == sfx_empty) { return; } EChanFlags oldflags = chan->ChanFlags; int startflags = 0; if (chan->ChanFlags & CHANF_LOOP) startflags |= SNDF_LOOP; if (chan->ChanFlags & CHANF_AREA) startflags |= SNDF_AREA; if (chan->ChanFlags & (CHANF_UI|CHANF_NOPAUSE)) startflags |= SNDF_NOPAUSE; if (chan->ChanFlags & CHANF_ABSTIME) startflags |= SNDF_ABSTIME; if (chan->ChanFlags & CHANF_IS3D) { FVector3 pos, vel; CalcPosVel(chan, &pos, &vel); if (!ValidatePosVel(chan, pos, vel)) { return; } // If this sound doesn't like playing near itself, don't play it if // that's what would happen. if (chan->NearLimit > 0 && CheckSoundLimit(&S_sfx[chan->SoundID], pos, chan->NearLimit, chan->LimitRange, 0, NULL, 0)) { return; } chan->ChanFlags &= ~(CHANF_EVICTED|CHANF_ABSTIME); ochan = (FSoundChan*)GSnd->StartSound3D(sfx->data, &listener, chan->Volume, &chan->Rolloff, chan->DistanceScale, chan->Pitch, chan->Priority, pos, vel, chan->EntChannel, startflags, chan); } else { chan->ChanFlags &= ~(CHANF_EVICTED|CHANF_ABSTIME); ochan = (FSoundChan*)GSnd->StartSound(sfx->data, chan->Volume, chan->Pitch, startflags, chan); } assert(ochan == NULL || ochan == chan); if (ochan == NULL) { chan->ChanFlags = oldflags; } } //========================================================================== // // S_LoadSound // // Returns a pointer to the sfxinfo with the actual sound data. // //========================================================================== sfxinfo_t *SoundEngine::LoadSound(sfxinfo_t *sfx) { if (GSnd->IsNull()) return sfx; while (!sfx->data.isValid()) { unsigned int i; // If the sound doesn't exist, replace it with the empty sound. if (sfx->lumpnum == -1) { sfx->lumpnum = sfx_empty; } // See if there is another sound already initialized with this lump. If so, // then set this one up as a link, and don't load the sound again. for (i = 0; i < S_sfx.Size(); i++) { if (S_sfx[i].data.isValid() && S_sfx[i].link == sfxinfo_t::NO_LINK && S_sfx[i].lumpnum == sfx->lumpnum && (!sfx->bLoadRAW || (sfx->RawRate == S_sfx[i].RawRate))) // Raw sounds with different sample rates may not share buffers, even if they use the same source data. { //DPrintf (DMSG_NOTIFY, "Linked %s to %s (%d)\n", sfx->name.GetChars(), S_sfx[i].name.GetChars(), i); sfx->link = i; // This is necessary to avoid using the rolloff settings of the linked sound if its // settings are different. if (sfx->Rolloff.MinDistance == 0) sfx->Rolloff = S_Rolloff; return &S_sfx[i]; } } //DPrintf(DMSG_NOTIFY, "Loading sound \"%s\" (%td)\n", sfx->name.GetChars(), sfx - &S_sfx[0]); auto sfxdata = ReadSound(sfx->lumpnum); int size = sfxdata.Size(); if (size > 8) { int32_t dmxlen = LittleLong(((int32_t *)sfxdata.Data())[1]); // If the sound is voc, use the custom loader. if (strncmp ((const char *)sfxdata.Data(), "Creative Voice File", 19) == 0) { sfx->data = GSnd->LoadSoundVoc(sfxdata.Data(), size); } // If the sound is raw, just load it as such. else if (sfx->bLoadRAW) { sfx->data = GSnd->LoadSoundRaw(sfxdata.Data(), size, sfx->RawRate, 1, 8, sfx->LoopStart); } // Otherwise, try the sound as DMX format. else if (((uint8_t *)sfxdata.Data())[0] == 3 && ((uint8_t *)sfxdata.Data())[1] == 0 && dmxlen <= size - 8) { int frequency = LittleShort(((uint16_t *)sfxdata.Data())[1]); if (frequency == 0) frequency = 11025; sfx->data = GSnd->LoadSoundRaw(sfxdata.Data()+8, dmxlen, frequency, 1, 8, sfx->LoopStart); } // If that fails, let the sound system try and figure it out. else { sfx->data = GSnd->LoadSound(sfxdata.Data(), size); } } if (!sfx->data.isValid()) { if (sfx->lumpnum != sfx_empty) { sfx->lumpnum = sfx_empty; continue; } } break; } return sfx; } //========================================================================== // // S_CheckSingular // // Returns true if a copy of this sound is already playing. // //========================================================================== bool SoundEngine::CheckSingular(int sound_id) { for (FSoundChan *chan = Channels; chan != NULL; chan = chan->NextChan) { if (chan->OrgID == sound_id) { return true; } } return false; } //========================================================================== // // S_CheckSoundLimit // // Limits the number of nearby copies of a sound that can play near // each other. If there are NearLimit instances of this sound already // playing within sqrt(limit_range) (typically 256 units) of the new sound, the // new sound will not start. // // If an actor is specified, and it is already playing the same sound on // the same channel, this sound will not be limited. In this case, we're // restarting an already playing sound, so there's no need to limit it. // // Returns true if the sound should not play. // //========================================================================== bool SoundEngine::CheckSoundLimit(sfxinfo_t *sfx, const FVector3 &pos, int near_limit, float limit_range, int sourcetype, const void *actor, int channel) { FSoundChan *chan; int count; for (chan = Channels, count = 0; chan != NULL && count < near_limit; chan = chan->NextChan) { if (chan->ChanFlags & CHANF_FORGETTABLE) continue; if (!(chan->ChanFlags & CHANF_EVICTED) && &S_sfx[chan->SoundID] == sfx) { FVector3 chanorigin; if (actor != NULL && chan->EntChannel == channel && chan->SourceType == sourcetype && chan->Source == actor) { // We are restarting a playing sound. Always let it play. return false; } CalcPosVel(chan, &chanorigin, NULL); if ((chanorigin - pos).LengthSquared() <= limit_range) { count++; } } } return count >= near_limit; } //========================================================================== // // S_StopSound // // Stops an unpositioned sound from playing on a specific channel. // //========================================================================== void SoundEngine::StopSoundID(int sound_id) { FSoundChan* chan = Channels; while (chan != NULL) { FSoundChan* next = chan->NextChan; if (sound_id == chan->OrgID) { StopChannel(chan); } chan = next; } } //========================================================================== // // S_StopSound // // Stops an unpositioned sound from playing on a specific channel. // //========================================================================== void SoundEngine::StopSound (int channel, int sound_id) { FSoundChan *chan = Channels; while (chan != NULL) { FSoundChan *next = chan->NextChan; if ((chan->SourceType == SOURCE_None && (sound_id == -1 || sound_id == chan->OrgID)) && (channel == CHAN_AUTO || channel == chan->EntChannel)) { StopChannel(chan); } chan = next; } } //========================================================================== // // S_StopSound // // Stops a sound from a single actor from playing on a specific channel. // //========================================================================== void SoundEngine::StopSound(int sourcetype, const void* actor, int channel, int sound_id) { FSoundChan* chan = Channels; while (chan != NULL) { FSoundChan* next = chan->NextChan; if (chan->SourceType == sourcetype && chan->Source == actor && (sound_id == -1? (chan->EntChannel == channel || channel < 0) : (chan->OrgID == sound_id))) { StopChannel(chan); } chan = next; } } //========================================================================== // // S_StopAllChannels // //========================================================================== void SoundEngine::StopAllChannels () { FSoundChan *chan = Channels; while (chan != NULL) { FSoundChan *next = chan->NextChan; StopChannel(chan); chan = next; } if (GSnd) GSnd->UpdateSounds(); } //========================================================================== // // S_RelinkSound // // Moves all the sounds from one thing to another. If the destination is // NULL, then the sound becomes a positioned sound. //========================================================================== void SoundEngine::RelinkSound (int sourcetype, const void *from, const void *to, const FVector3 *optpos) { if (from == NULL) return; FSoundChan *chan = Channels; while (chan != NULL) { FSoundChan *next = chan->NextChan; if (chan->SourceType == sourcetype && chan->Source == from) { if (to != NULL) { chan->Source = to; } else if (!(chan->ChanFlags & CHANF_LOOP) && optpos) { chan->Source = NULL; chan->SourceType = SOURCE_Unattached; chan->Point[0] = optpos->X; chan->Point[1] = optpos->Y; chan->Point[2] = optpos->Z; } else { StopChannel(chan); } } chan = next; } } //========================================================================== // // S_ChangeSoundVolume // //========================================================================== void SoundEngine::ChangeSoundVolume(int sourcetype, const void *source, int channel, double dvolume) { float volume = float(dvolume); // don't let volume get out of bounds if (volume < 0.0) volume = 0.0; else if (volume > 1.0) volume = 1.0; for (FSoundChan *chan = Channels; chan != NULL; chan = chan->NextChan) { if (chan->SourceType == sourcetype && chan->Source == source && (chan->EntChannel == channel || channel == -1)) { GSnd->ChannelVolume(chan, volume); chan->Volume = volume; } } return; } void SoundEngine::SetVolume(FSoundChan* chan, float volume) { if (volume < 0.0) volume = 0.0; else if (volume > 1.0) volume = 1.0; assert(chan != nullptr); GSnd->ChannelVolume(chan, volume); chan->Volume = volume; } //========================================================================== // // S_ChangeSoundPitch // //========================================================================== void SoundEngine::ChangeSoundPitch(int sourcetype, const void *source, int channel, double pitch, int sound_id) { for (FSoundChan *chan = Channels; chan != NULL; chan = chan->NextChan) { if (chan->SourceType == sourcetype && chan->Source == source && (sound_id == -1? (chan->EntChannel == channel) : (chan->OrgID == sound_id))) { SetPitch(chan, (float)pitch); } } return; } void SoundEngine::SetPitch(FSoundChan *chan, float pitch) { assert(chan != nullptr); GSnd->ChannelPitch(chan, std::max(0.0001f, pitch)); chan->Pitch = std::max(1, int(float(DEFAULT_PITCH) * pitch)); } //========================================================================== // // S_GetSoundPlayingInfo // // Is a sound being played by a specific emitter? //========================================================================== int SoundEngine::GetSoundPlayingInfo (int sourcetype, const void *source, int sound_id) { int count = 0; if (sound_id > 0) { for (FSoundChan *chan = Channels; chan != NULL; chan = chan->NextChan) { if (chan->OrgID == sound_id && (sourcetype == SOURCE_Any || (chan->SourceType == sourcetype && chan->Source == source))) { count++; } } } return count; } //========================================================================== // // S_IsChannelUsed // // Returns true if the channel is in use. Also fills in a bitmask of // channels seen while scanning for this one, to make searching for unused // channels faster. Initialize seen to 0 for the first call. // //========================================================================== bool SoundEngine::IsChannelUsed(int sourcetype, const void *actor, int channel, int *seen) { if (*seen & (1 << channel)) { return true; } for (FSoundChan *chan = Channels; chan != NULL; chan = chan->NextChan) { if (chan->SourceType == sourcetype && chan->Source == actor) { *seen |= 1 << chan->EntChannel; if (chan->EntChannel == channel) { return true; } } } return false; } //========================================================================== // // S_IsActorPlayingSomething // //========================================================================== bool SoundEngine::IsSourcePlayingSomething (int sourcetype, const void *actor, int channel, int sound_id) { for (FSoundChan *chan = Channels; chan != NULL; chan = chan->NextChan) { if (chan->SourceType == sourcetype && (sourcetype == SOURCE_None || sourcetype == SOURCE_Unattached || chan->Source == actor)) { if ((channel == 0 || chan->EntChannel == channel) && (sound_id <= 0 || chan->OrgID == sound_id)) { return true; } } } return false; } //========================================================================== // // S_EvictAllChannels // // Forcibly evicts all channels so that there are none playing, but all // information needed to restart them is retained. // //========================================================================== void SoundEngine::EvictAllChannels() { FSoundChan *chan, *next; for (chan = Channels; chan != NULL; chan = next) { next = chan->NextChan; if (!(chan->ChanFlags & CHANF_EVICTED)) { chan->ChanFlags |= CHANF_EVICTED; if (chan->SysChannel != NULL) { if (!(chan->ChanFlags & CHANF_ABSTIME)) { chan->StartTime = GSnd ? GSnd->GetPosition(chan) : 0; chan->ChanFlags |= CHANF_ABSTIME; } StopChannel(chan); } // assert(chan->NextChan == next); } } } //========================================================================== // // S_RestoreEvictedChannel // // Recursive helper for S_RestoreEvictedChannels(). // //========================================================================== void SoundEngine::RestoreEvictedChannel(FSoundChan *chan) { if (chan == NULL) { return; } RestoreEvictedChannel(chan->NextChan); if (chan->ChanFlags & CHANF_EVICTED) { RestartChannel(chan); if (!(chan->ChanFlags & CHANF_LOOP)) { if (chan->ChanFlags & CHANF_EVICTED) { // Still evicted and not looping? Forget about it. ReturnChannel(chan); } else if (!(chan->ChanFlags & CHANF_JUSTSTARTED)) { // Should this sound become evicted again, it's okay to forget about it. chan->ChanFlags |= CHANF_FORGETTABLE; } } } else if (chan->SysChannel == NULL && (chan->ChanFlags & (CHANF_FORGETTABLE | CHANF_LOOP)) == CHANF_FORGETTABLE) { ReturnChannel(chan); } } //========================================================================== // // S_RestoreEvictedChannels // // Restarts as many evicted channels as possible. Any channels that could // not be started and are not looping are moved to the free pool. // //========================================================================== void SoundEngine::RestoreEvictedChannels() { // Restart channels in the same order they were originally played. RestoreEvictedChannel(Channels); } //========================================================================== // // S_UpdateSounds // // Updates music & sounds //========================================================================== void SoundEngine::UpdateSounds(int time) { FVector3 pos, vel; for (FSoundChan* chan = Channels; chan != NULL; chan = chan->NextChan) { if ((chan->ChanFlags & (CHANF_EVICTED | CHANF_IS3D)) == CHANF_IS3D) { CalcPosVel(chan, &pos, &vel); if (ValidatePosVel(chan, pos, vel)) { GSnd->UpdateSoundParams3D(&listener, chan, !!(chan->ChanFlags & CHANF_AREA), pos, vel); } } chan->ChanFlags &= ~CHANF_JUSTSTARTED; } GSnd->UpdateListener(&listener); GSnd->UpdateSounds(); if (time >= RestartEvictionsAt) { RestartEvictionsAt = 0; RestoreEvictedChannels(); } } //========================================================================== // // S_GetRolloff // //========================================================================== float SoundEngine::GetRolloff(const FRolloffInfo* rolloff, float distance) { if (rolloff == NULL) { return 0; } if (distance <= rolloff->MinDistance) { return 1.f; } // Logarithmic rolloff has no max distance where it goes silent. if (rolloff->RolloffType == ROLLOFF_Log) { return rolloff->MinDistance / (rolloff->MinDistance + rolloff->RolloffFactor * (distance - rolloff->MinDistance)); } if (distance >= rolloff->MaxDistance) { return 0.f; } float volume = (rolloff->MaxDistance - distance) / (rolloff->MaxDistance - rolloff->MinDistance); if (rolloff->RolloffType == ROLLOFF_Linear) { return volume; } if (rolloff->RolloffType == ROLLOFF_Custom && S_SoundCurve.Size() > 0) { return S_SoundCurve[int(S_SoundCurve.Size() * (1.f - volume))] / (float)SndCurveFactor; } return (powf(10.f, volume) - 1.f) / 9.f; } //========================================================================== // // S_ChannelEnded (callback for sound interface code) // //========================================================================== void SoundEngine::ChannelEnded(FISoundChannel *ichan) { FSoundChan *schan = static_cast(ichan); bool evicted; schan->ChanFlags &= ~CHANF_ENDED; if (schan != NULL) { // If the sound was stopped with GSnd->StopSound(), then we know // it wasn't evicted. Otherwise, if it's looping, it must have // been evicted. If it's not looping, then it was evicted if it // didn't reach the end of its playback. if (schan->ChanFlags & CHANF_FORGETTABLE) { evicted = false; } else if (schan->ChanFlags & (CHANF_LOOP | CHANF_EVICTED)) { evicted = true; } else { unsigned int pos = GSnd->GetPosition(schan); unsigned int len = GSnd->GetSampleLength(S_sfx[schan->SoundID].data); if (pos == 0) { evicted = !!(schan->ChanFlags & CHANF_JUSTSTARTED); } else { evicted = (pos < len); } } if (!evicted) { ReturnChannel(schan); } else { schan->ChanFlags |= CHANF_EVICTED; schan->SysChannel = NULL; } } } //========================================================================== // // S_ChannelVirtualChanged (callback for sound interface code) // //========================================================================== void SoundEngine::ChannelVirtualChanged(FISoundChannel *ichan, bool is_virtual) { FSoundChan *schan = static_cast(ichan); if (is_virtual) { schan->ChanFlags |= CHANF_VIRTUAL; } else { schan->ChanFlags &= ~CHANF_VIRTUAL; } } //========================================================================== // // StopChannel // //========================================================================== void SoundEngine::StopChannel(FSoundChan *chan) { if (chan == NULL) return; if (chan->SysChannel != NULL) { // S_EvictAllChannels() will set the CHAN_EVICTED flag to indicate // that it wants to keep all the channel information around. if (!(chan->ChanFlags & CHANF_EVICTED)) { chan->ChanFlags |= CHANF_FORGETTABLE; if (chan->SourceType == SOURCE_Actor) { chan->Source = NULL; chan->SourceType = SOURCE_Unattached; } } if (GSnd) GSnd->StopChannel(chan); } else { ReturnChannel(chan); } } void SoundEngine::UnloadAllSounds() { for (unsigned i = 0; i < S_sfx.Size(); i++) { UnloadSound(&S_sfx[i]); } } void SoundEngine::Reset() { EvictAllChannels(); I_CloseSound(); I_InitSound(); RestoreEvictedChannels(); } //========================================================================== // // S_FindSound // // Given a logical name, find the sound's index in S_sfx. //========================================================================== int SoundEngine::FindSound(const char* logicalname) { int i; if (logicalname != NULL) { i = S_sfx[MakeKey(logicalname) % S_sfx.Size()].index; while ((i != 0) && stricmp(S_sfx[i].name, logicalname)) i = S_sfx[i].next; return i; } else { return 0; } } int SoundEngine::FindSoundByResID(int resid) { auto p = ResIdMap.CheckKey(resid); return p ? *p : 0; } //========================================================================== // // S_FindSoundNoHash // // Given a logical name, find the sound's index in S_sfx without // using the hash table. //========================================================================== int SoundEngine::FindSoundNoHash(const char* logicalname) { unsigned int i; for (i = 1; i < S_sfx.Size(); i++) { if (stricmp(S_sfx[i].name, logicalname) == 0) { return i; } } return 0; } //========================================================================== // // S_FindSoundByLump // // Given a sound lump, find the sound's index in S_sfx. //========================================================================== int SoundEngine::FindSoundByLump(int lump) { if (lump != -1) { unsigned int i; for (i = 1; i < S_sfx.Size(); i++) if (S_sfx[i].lumpnum == lump) return i; } return 0; } //========================================================================== // // S_AddSoundLump // // Adds a new sound mapping to S_sfx. //========================================================================== int SoundEngine::AddSoundLump(const char* logicalname, int lump, int CurrentPitchMask, int resid, int nearlimit) { S_sfx.Reserve(1); sfxinfo_t &newsfx = S_sfx.Last(); newsfx.data.Clear(); newsfx.name = logicalname; newsfx.lumpnum = lump; newsfx.next = 0; newsfx.index = 0; newsfx.Volume = 1; newsfx.Attenuation = 1; newsfx.PitchMask = CurrentPitchMask; newsfx.NearLimit = nearlimit; newsfx.LimitRange = 256 * 256; newsfx.bRandomHeader = false; newsfx.bLoadRAW = false; newsfx.b16bit = false; newsfx.bUsed = false; newsfx.bSingular = false; newsfx.bTentative = false; newsfx.ResourceId = resid; newsfx.RawRate = 0; newsfx.link = sfxinfo_t::NO_LINK; newsfx.Rolloff.RolloffType = ROLLOFF_Doom; newsfx.Rolloff.MinDistance = 0; newsfx.Rolloff.MaxDistance = 0; newsfx.LoopStart = -1; if (resid >= 0) ResIdMap[resid] = S_sfx.Size() - 1; return (int)S_sfx.Size()-1; } int SoundEngine::AddSfx(sfxinfo_t &sfx) { S_sfx.Push(sfx); if (sfx.ResourceId >= 0) ResIdMap[sfx.ResourceId] = S_sfx.Size() - 1; return (int)S_sfx.Size() - 1; } //========================================================================== // // S_FindSoundTentative // // Given a logical name, find the sound's index in S_sfx without // using the hash table. If it does not exist, a new sound without // an associated lump is created. //========================================================================== int SoundEngine::FindSoundTentative(const char* name) { int id = FindSoundNoHash(name); if (id == 0) { id = AddSoundLump(name, -1, 0); S_sfx[id].bTentative = true; } return id; } //========================================================================== // // S_CacheRandomSound // // Loads all sounds a random sound might play. // //========================================================================== void SoundEngine::CacheRandomSound(sfxinfo_t* sfx) { if (sfx->bRandomHeader) { const FRandomSoundList* list = &S_rnd[sfx->link]; for (unsigned i = 0; i < list->Choices.Size(); ++i) { sfx = &S_sfx[list->Choices[i]]; sfx->bUsed = true; CacheSound(&S_sfx[list->Choices[i]]); } } } //========================================================================== // // S_GetSoundMSLength // // Returns duration of sound // GZDoom does not use this due to player sound handling // //========================================================================== unsigned int SoundEngine::GetMSLength(FSoundID sound) { if ((unsigned int)sound >= S_sfx.Size()) { return 0; } sfxinfo_t* sfx = &S_sfx[sound]; // Resolve player sounds, random sounds, and aliases if (sfx->link != sfxinfo_t::NO_LINK) { if (sfx->bRandomHeader) { // Hm... What should we do here? // Pick the longest or the shortest sound? // I think the longest one makes more sense. int length = 0; const FRandomSoundList* list = &S_rnd[sfx->link]; for (auto& me : list->Choices) { // unfortunately we must load all sounds to find the longest one... :( int thislen = GetMSLength(me); if (thislen > length) length = thislen; } return length; } else { sfx = &S_sfx[sfx->link]; } } sfx = LoadSound(sfx); if (sfx != NULL) return GSnd->GetMSLength(sfx->data); else return 0; } //========================================================================== // // S_PickReplacement // // Picks a replacement sound from the associated random list. If this sound // is not the head of a random list, then the sound passed is returned. //========================================================================== int SoundEngine::PickReplacement(int refid) { while (S_sfx[refid].bRandomHeader) { const FRandomSoundList* list = &S_rnd[S_sfx[refid].link]; refid = list->Choices[rand() % int(list->Choices.Size())]; } return refid; } //========================================================================== // // S_HashSounds // // Fills in the next and index fields of S_sfx to form a working hash table. //========================================================================== void SoundEngine::HashSounds() { unsigned int i; unsigned int j; unsigned int size; S_sfx.ShrinkToFit(); size = S_sfx.Size(); // Mark all buckets as empty for (i = 0; i < size; i++) S_sfx[i].index = 0; // Now set up the chains for (i = 1; i < size; i++) { j = MakeKey(S_sfx[i].name) % size; S_sfx[i].next = S_sfx[j].index; S_sfx[j].index = i; } S_rnd.ShrinkToFit(); } void SoundEngine::AddRandomSound(int Owner, TArray list) { auto index = S_rnd.Reserve(1); auto& random = S_rnd.Last(); random.Choices = std::move(list); random.Owner = Owner; S_sfx[Owner].link = index; S_sfx[Owner].bRandomHeader = true; S_sfx[Owner].NearLimit = -1; } extern ReverbContainer* ForcedEnvironment; static int LastReverb; CUSTOM_CVAR(Bool, snd_reverb, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) { FX_SetReverb(-1); } // This is for testing reverb settings. CUSTOM_CVAR(Int, snd_reverbtype, -1, 0) { FX_SetReverb(-1); } void FX_SetReverb(int strength) { if (strength == -1) strength = LastReverb; if (snd_reverbtype > -1) { strength = snd_reverbtype; ForcedEnvironment = S_FindEnvironment(strength); } else if (snd_reverb && strength > 0) { // todo: optimize environments. The original "reverb" was garbage and not usable as reference. if (strength < 64) strength = 0x1400; else if (strength < 192) strength = 0x1503; else strength = 0x1900; LastReverb = strength; ForcedEnvironment = S_FindEnvironment(strength); } else ForcedEnvironment = nullptr; } #include "basics.h" #include "stats.h" #include "v_text.h" //========================================================================== // // S_NoiseDebug // // [RH] Print sound debug info. Called by status bar. //========================================================================== FString SoundEngine::NoiseDebug() { FVector3 listener; FVector3 origin; listener = this->listener.position; int ch = 0; FString out; out.Format("*** SOUND DEBUG INFO ***\nListener: %3.2f %2.3f %2.3f\n" "x y z vol dist chan pri flags aud pos name\n", listener.X, listener.Y, listener.Z); for (auto chan = Channels; chan; chan = chan->NextChan) { if (!(chan->ChanFlags & CHANF_IS3D)) { out += "--- --- --- --- "; } else { CalcPosVel(chan, &origin, nullptr); out.AppendFormat(TEXTCOLOR_GOLD "%5.0f | %5.0f | %5.0f | %5.0f ", origin.X, origin.Z, origin.Y, (origin - listener).Length()); } out.AppendFormat("%-.2g %-4d %-4d %sF%s3%sZ%sU%sM%sN%sA%sL%sE%sV" TEXTCOLOR_GOLD " %-5.4f %-4u %d: %s %p\n", chan->Volume, chan->EntChannel, chan->Priority, (chan->ChanFlags & CHANF_FORGETTABLE) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, (chan->ChanFlags & CHANF_IS3D) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, (chan->ChanFlags & CHANF_LISTENERZ) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, (chan->ChanFlags & CHANF_UI) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, (chan->ChanFlags & CHANF_MAYBE_LOCAL) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, (chan->ChanFlags & CHANF_NOPAUSE) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, (chan->ChanFlags & CHANF_AREA) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, (chan->ChanFlags & CHANF_LOOP) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, (chan->ChanFlags & CHANF_EVICTED) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, (chan->ChanFlags & CHANF_VIRTUAL) ? TEXTCOLOR_GREEN : TEXTCOLOR_DARKRED, GSnd->GetAudibility(chan), GSnd->GetPosition(chan), ((int)chan->OrgID)-1, S_sfx[chan->SoundID].name.GetChars(), chan->Source); ch++; } out.AppendFormat("%d channels\n", ch); return out; } ADD_STAT(sounddebug) { return soundEngine->NoiseDebug(); } CVAR(Bool, snd_extendedlookup, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) int S_LookupSound(const char* fn) { static const FName sndformats[] = { NAME_OGG, NAME_FLAC, NAME_WAV }; if (snd_extendedlookup) { int lump = fileSystem.FindFileWithExtensions(StripExtension(fn), sndformats, countof(sndformats)); if (lump >= 0) return lump; } return fileSystem.FindFile(fn); } //========================================================================== // // Although saving the sound system's state is supposed to be an engine // feature, the specifics cannot be set up there, this needs to be on the client side. // //========================================================================== static FSerializer& Serialize(FSerializer& arc, const char* key, FSoundChan& chan, FSoundChan* def) { if (arc.BeginObject(key)) { arc("sourcetype", chan.SourceType) ("soundid", chan.SoundID) ("orgid", chan.OrgID) ("volume", chan.Volume) ("distancescale", chan.DistanceScale) ("pitch", chan.Pitch) ("chanflags", chan.ChanFlags) ("entchannel", chan.EntChannel) ("priority", chan.Priority) ("nearlimit", chan.NearLimit) ("starttime", chan.StartTime) ("rolloftype", chan.Rolloff.RolloffType) ("rolloffmin", chan.Rolloff.MinDistance) ("rolloffmax", chan.Rolloff.MaxDistance) ("limitrange", chan.LimitRange) .Array("point", chan.Point, 3); int SourceIndex = 0; if (arc.isWriting()) { if (chan.SourceType == SOURCE_Actor) SourceIndex = int((spritetype*)(chan.Source) - sprite); else SourceIndex = soundEngine->SoundSourceIndex(&chan); } arc("Source", SourceIndex); if (arc.isReading()) { if (chan.SourceType == SOURCE_Actor) chan.Source = &sprite[SourceIndex]; else soundEngine->SetSource(&chan, SourceIndex); } arc.EndObject(); } return arc; } //========================================================================== // // S_SerializeSounds // //========================================================================== void S_SerializeSounds(FSerializer& arc) { FSoundChan* chan; GSnd->Sync(true); if (arc.isWriting()) { // Count channels and accumulate them so we can store them in // reverse order. That way, they will be in the same order when // reloaded later as they are now. TArray chans = soundEngine->AllActiveChannels(); if (chans.Size() > 0 && arc.BeginArray("sounds")) { for (unsigned int i = chans.Size(); i-- != 0; ) { // Replace start time with sample position. uint64_t start = chans[i]->StartTime; chans[i]->StartTime = GSnd ? GSnd->GetPosition(chans[i]) : 0; arc(nullptr, *chans[i]); chans[i]->StartTime = start; } arc.EndArray(); } } else { unsigned int count; soundEngine->StopAllChannels(); if (arc.BeginArray("sounds")) { count = arc.ArraySize(); for (unsigned int i = 0; i < count; ++i) { chan = (FSoundChan*)soundEngine->GetChannel(nullptr); arc(nullptr, *chan); // Sounds always start out evicted when restored from a save. chan->ChanFlags |= CHANF_EVICTED | CHANF_ABSTIME; } arc.EndArray(); } // totalclock runs on 120 fps, we need to allow a small delay here. soundEngine->SetRestartTime((int)totalclock + 6); } GSnd->Sync(false); GSnd->UpdateSounds(); }