raze/source/common/audio/sound/s_sound.cpp
Christoph Oelckers 800e7939b8 - fixed: when deactivating the app, pausing the sound system should not depend on the game's internal pause state.
All this tells is that the gameplay was halted. Sound was not stopped when the user either minimized or tabbed away in the in-game menu.
2022-11-11 18:40:36 +01:00

1861 lines
46 KiB
C++

/*
** 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 <stdio.h>
#include <stdlib.h>
#include "s_soundinternal.h"
#include "m_swap.h"
#include "superfasthash.h"
#include "s_music.h"
#include "m_random.h"
#include "printf.h"
#include "c_cvars.h"
#include "gamestate.h"
CVARD(Bool, snd_enabled, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG, "enables/disables sound effects")
CVAR(Bool, i_soundinbackground, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
CVAR(Bool, i_pauseinbackground, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
int SoundEnabled()
{
return snd_enabled && !nosound && !nosfx;
}
enum
{
DEFAULT_PITCH = 128,
};
static FRandom pr_soundpitch ("SoundPitch");
SoundEngine* soundEngine;
//==========================================================================
//
// S_Init
//
//==========================================================================
void SoundEngine::Init(TArray<uint8_t> &curve)
{
StopAllChannels();
// Free all channels for use.
while (Channels != NULL)
{
ReturnChannel(Channels);
}
S_SoundCurve = std::move(curve);
}
//==========================================================================
//
// 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)
{
while (!sfx->bRandomHeader && sfx->link != sfxinfo_t::NO_LINK)
{
sfx = &S_sfx[sfx->link];
}
if (sfx->bRandomHeader)
{
CacheRandomSound(sfx);
}
else
{
LoadSound(sfx);
sfx->bUsed = true;
}
}
}
//==========================================================================
//
// S_UnloadSound
//
//==========================================================================
void SoundEngine::UnloadSound (sfxinfo_t *sfx)
{
if (sfx->data.isValid())
{
GSnd->UnloadSound(sfx->data);
DPrintf(DMSG_NOTIFY, "Unloaded sound \"%s\" (%td)\n", sfx->name.GetChars(), sfx - &S_sfx[0]);
}
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<FSoundChan*> SoundEngine::AllActiveChannels()
{
TArray<FSoundChan*> 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, float startTime)
{
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 || nosfx || !SoundEnabled() || blockNewSounds || (unsigned)sound_id >= S_sfx.Size())
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 = 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;
float defpitch = sfx->DefPitch;
float defpitchmax = sfx->DefPitchMax;
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;
defpitch = newsfx->DefPitch;
defpitchmax = newsfx->DefPitchMax;
}
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.
if (near_limit > 0 && CheckSoundLimit(sfx, pos, near_limit, limit_range, type, source, channel, attenuation))
{
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)
{
// In the old sound system, 'AUTO' hijacked one of the other channels.
// Now, with CHANF_OVERLAP at our disposal that isn't needed anymore. Just set the flag and let all sounds play on channel 0.
chanflags |= CHANF_OVERLAP;
}
// 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|CHANF_FORCE)) && SoundPaused)
{
return NULL;
}
// Vary the sfx pitches. Overridden by $PitchSet and A_StartSound.
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;
float sfxlength = (float)GSnd->GetMSLength(sfx->data) / 1000.f;
startTime = (startflags & SNDF_LOOP)
? (sfxlength > 0 ? fmodf(startTime, sfxlength) : 0.f)
: clamp(startTime, 0.f, sfxlength);
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, startTime);
}
else
{
chan = (FSoundChan*)GSnd->StartSound (sfx->data, float(volume), pitch, startflags, NULL, startTime);
}
}
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;
chan->UserData = 0;
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) // A_StartSound has top priority over all others.
SetPitch(chan, spitch);
else if (defpitch > 0.0) // $PitchSet overrides $PitchShift
{
if (defpitchmax > 0.0)
{
if (defpitchmax < defpitch)
std::swap(defpitch, defpitchmax);
if (defpitch != defpitchmax)
{
FRandom &rng = pr_soundpitch;
int random = (rng)(0x7FFF);
float frandom = random / float(0x7FFF);
defpitch = frandom * (defpitchmax - defpitch) + defpitch;
}
}
SetPitch(chan, defpitch);
}
}
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, chan->DistanceScale))
{
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 (sfx->lumpnum == sfx_empty)
{
return sfx;
}
// 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, float attenuation)
{
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);
// scale the limit distance with the attenuation. An attenuation of 0 means the limit distance is infinite and all sounds within the level are inside the limit.
float attn = min(chan->DistanceScale, attenuation);
if (attn <= 0 || (chanorigin - pos).LengthSquared() <= limit_range / attn)
{
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_StopAllActorSounds
//
// Stops all sounds on an actor.
//
//==========================================================================
void SoundEngine::StopActorSounds(int sourcetype, const void* actor, int chanmin, int chanmax)
{
const bool all = (chanmin == 0 && chanmax == 0);
if (chanmax < chanmin) std::swap(chanmin, chanmax);
FSoundChan* chan = Channels;
while (chan != nullptr)
{
FSoundChan* next = chan->NextChan;
if (chan->SourceType == sourcetype &&
chan->Source == actor &&
(all || (chan->EntChannel >= chanmin && chan->EntChannel <= chanmax)))
{
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, max(0.0001f, pitch));
chan->Pitch = 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 chann)
{
int count = 0;
if (sound_id > 0)
{
for (FSoundChan *chan = Channels; chan != NULL; chan = chan->NextChan)
{
if (chann != -1 && chann != chan->EntChannel) continue;
if (chan->OrgID == sound_id && (sourcetype == SOURCE_Any ||
(chan->SourceType == sourcetype &&
chan->Source == source)))
{
count++;
}
}
}
else
{
for (FSoundChan* chan = Channels; chan != NULL; chan = chan->NextChan)
{
if (chann != -1 && chann != chan->EntChannel) continue;
if ((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))] / 127.f;
}
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<FSoundChan*>(ichan);
bool evicted;
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)
{
schan->ChanFlags &= ~CHANF_EVICTED;
}
else
{
schan->ChanFlags |= CHANF_EVICTED;
schan->SysChannel = NULL;
}
}
}
//==========================================================================
//
//
//
//==========================================================================
void SoundEngine::SoundDone(FISoundChannel* ichan)
{
FSoundChan* schan = static_cast<FSoundChan*>(ichan);
if (schan != NULL)
{
ReturnChannel(schan);
}
}
//==========================================================================
//
// S_ChannelVirtualChanged (callback for sound interface code)
//
//==========================================================================
void SoundEngine::ChannelVirtualChanged(FISoundChannel *ichan, bool is_virtual)
{
FSoundChan *schan = static_cast<FSoundChan*>(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 (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.name = logicalname;
newsfx.lumpnum = lump;
newsfx.next = 0;
newsfx.PitchMask = CurrentPitchMask;
newsfx.NearLimit = nearlimit;
newsfx.ResourceId = resid;
newsfx.bTentative = false;
if (resid >= 0) ResIdMap[resid] = 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<uint32_t> 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;
}
void S_SoundReset()
{
S_StopMusic(true);
soundEngine->Reset();
S_RestartMusic();
}
//==========================================================================
//
// CCMD cachesound <sound name>
//
//==========================================================================
#include "s_music.h"
#include "vm.h"
#include "c_dispatch.h"
#include "stats.h"
#include "i_net.h"
#include "i_interface.h"
CCMD(cachesound)
{
if (argv.argc() < 2)
{
Printf("Usage: cachesound <sound> ...\n");
return;
}
for (int i = 1; i < argv.argc(); ++i)
{
FSoundID sfxnum = argv[i];
if (sfxnum != FSoundID(0))
{
soundEngine->CacheSound(sfxnum);
}
}
}
CCMD(listsoundchannels)
{
Printf("%s", soundEngine->ListSoundChannels().GetChars());
}
// intentionally moved here to keep the s_music include out of the rest of the file.
//==========================================================================
//
// S_PauseSound
//
// Stop music and sound effects, during game PAUSE.
//==========================================================================
void S_PauseSound(bool notmusic, bool notsfx)
{
if (!notmusic)
{
S_PauseMusic();
}
if (!notsfx)
{
soundEngine->SetPaused(true);
GSnd->SetSfxPaused(true, 0);
}
}
DEFINE_ACTION_FUNCTION(DObject, S_PauseSound)
{
PARAM_PROLOGUE;
PARAM_BOOL(notmusic);
PARAM_BOOL(notsfx);
S_PauseSound(notmusic, notsfx);
return 0;
}
//==========================================================================
//
// S_ResumeSound
//
// Resume music and sound effects, after game PAUSE.
//==========================================================================
void S_ResumeSound(bool notsfx)
{
S_ResumeMusic();
if (!notsfx)
{
soundEngine->SetPaused(false);
GSnd->SetSfxPaused(false, 0);
}
}
DEFINE_ACTION_FUNCTION(DObject, S_ResumeSound)
{
PARAM_PROLOGUE;
PARAM_BOOL(notsfx);
S_ResumeSound(notsfx);
return 0;
}
//==========================================================================
//
// S_SetSoundPaused
//
// Called with state non-zero when the app is active, zero when it isn't.
//
//==========================================================================
void S_SetSoundPaused(int state)
{
if (!netgame && (i_pauseinbackground))
{
pauseext = !state;
}
if ((state || i_soundinbackground) && !pauseext)
{
S_ResumeSound(true);
if (GSnd != nullptr)
{
GSnd->SetInactive(SoundRenderer::INACTIVE_Active);
}
}
else
{
S_PauseSound(false, true);
if (GSnd != nullptr)
{
GSnd->SetInactive(gamestate == GS_LEVEL || gamestate == GS_TITLELEVEL ?
SoundRenderer::INACTIVE_Complete :
SoundRenderer::INACTIVE_Mute);
}
}
}
CCMD(snd_status)
{
GSnd->PrintStatus();
}
CCMD(snd_listdrivers)
{
GSnd->PrintDriversList();
}
ADD_STAT(sound)
{
return GSnd->GatherStats();
}