/*
===========================================================================

Doom 3 GPL Source Code
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.

This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").

Doom 3 Source Code 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 3 of the License, or
(at your option) any later version.

Doom 3 Source Code 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 Doom 3 Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code.  If not, please request a copy in writing from id Software at the address below.

If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.

===========================================================================
*/

#include "sys/platform.h"
#include "framework/FileSystem.h"
#include "framework/Session.h"
#include "renderer/RenderWorld.h"

#include "sound/snd_local.h"

/*
==================
idSoundWorldLocal::Init
==================
*/
void idSoundWorldLocal::Init( idRenderWorld *renderWorld ) {
	rw = renderWorld;
	writeDemo = NULL;

	listenerAxis.Identity();
	listenerPos.Zero();
	listenerPrivateId = 0;
	listenerQU.Zero();
	listenerArea = 0;
	listenerAreaName = "Undefined";

	if (idSoundSystemLocal::useEFXReverb) {
		if (!soundSystemLocal.alIsAuxiliaryEffectSlot(listenerSlot)) {
			alGetError();

			soundSystemLocal.alGenAuxiliaryEffectSlots(1, &listenerSlot);
			ALuint e = alGetError();
			if (e != AL_NO_ERROR) {
				common->Warning("idSoundWorldLocal::Init: alGenAuxiliaryEffectSlots failed: 0x%x", e);
				listenerSlot = AL_EFFECTSLOT_NULL;
			}
		}

		if (!listenerAreFiltersInitialized) {
			listenerAreFiltersInitialized = true;

			alGetError();
			soundSystemLocal.alGenFilters(2, listenerFilters);
			ALuint e = alGetError();
			if (e != AL_NO_ERROR) {
				common->Warning("idSoundWorldLocal::Init: alGenFilters failed: 0x%x", e);
				listenerFilters[0] = AL_FILTER_NULL;
				listenerFilters[1] = AL_FILTER_NULL;
			} else {
				soundSystemLocal.alFilteri(listenerFilters[0], AL_FILTER_TYPE, AL_FILTER_LOWPASS);
				// original EAX occusion value was -1150
				// default OCCLUSIONLFRATIO is 0.25
				// default OCCLUSIONDIRECTRATIO is 1.0

				// pow(10.0, (-1150*0.25*1.0)/2000.0)
				soundSystemLocal.alFilterf(listenerFilters[0], AL_LOWPASS_GAIN, 0.718208f);
				// pow(10.0, (-1150*1.0)/2000.0)
				soundSystemLocal.alFilterf(listenerFilters[0], AL_LOWPASS_GAINHF, 0.266073f);


				soundSystemLocal.alFilteri(listenerFilters[1], AL_FILTER_TYPE, AL_FILTER_LOWPASS);
				// original EAX occusion value was -1150
				// default OCCLUSIONLFRATIO is 0.25
				// default OCCLUSIONROOMRATIO is 1.5

				// pow(10.0, (-1150*(0.25+1.5-1.0))/2000.0)
				soundSystemLocal.alFilterf(listenerFilters[1], AL_LOWPASS_GAIN, 0.370467f);
				// pow(10.0, (-1150*1.5)/2000.0)
				soundSystemLocal.alFilterf(listenerFilters[1], AL_LOWPASS_GAINHF, 0.137246f);
			}
		}
	}

	gameMsec = 0;
	game44kHz = 0;
	pause44kHz = -1;
	lastAVI44kHz = 0;

	for ( int i = 0 ; i < SOUND_MAX_CLASSES ; i++ ) {
		soundClassFade[i].Clear();
	}

	// fill in the 0 index spot
	idSoundEmitterLocal	*placeHolder = new idSoundEmitterLocal;
	emitters.Append( placeHolder );

	fpa[0] = fpa[1] = fpa[2] = fpa[3] = fpa[4] = fpa[5] = NULL;

	aviDemoPath = "";
	aviDemoName = "";

	localSound = NULL;

	slowmoActive		= false;
	slowmoSpeed			= 0;
	enviroSuitActive	= false;
}

/*
===============
idSoundWorldLocal::idSoundWorldLocal
===============
*/
idSoundWorldLocal::idSoundWorldLocal() {
	listenerAreFiltersInitialized = false;
}

/*
===============
idSoundWorldLocal::~idSoundWorldLocal
===============
*/
idSoundWorldLocal::~idSoundWorldLocal() {
	Shutdown();
}

/*
===============
idSoundWorldLocal::Shutdown

  this is called from the main thread
===============
*/
void idSoundWorldLocal::Shutdown() {
	int i;

	if ( soundSystemLocal.currentSoundWorld == this ) {
		soundSystemLocal.currentSoundWorld = NULL;
	}

	AVIClose();

	if (idSoundSystemLocal::useEFXReverb) {
		if (soundSystemLocal.alIsAuxiliaryEffectSlot(listenerSlot)) {
			soundSystemLocal.alAuxiliaryEffectSloti(listenerSlot, AL_EFFECTSLOT_EFFECT, AL_EFFECTSLOT_NULL);
			soundSystemLocal.alDeleteAuxiliaryEffectSlots(1, &listenerSlot);
			listenerSlot = AL_EFFECTSLOT_NULL;
		}

		if (listenerAreFiltersInitialized) {
			listenerAreFiltersInitialized = false;

			if (listenerFilters[0] != AL_FILTER_NULL && listenerFilters[1] != AL_FILTER_NULL) {
				soundSystemLocal.alDeleteFilters(2, listenerFilters);
				listenerFilters[0] = AL_FILTER_NULL;
				listenerFilters[1] = AL_FILTER_NULL;
			}
		}
	}

	for ( i = 0; i < emitters.Num(); i++ ) {
		if ( emitters[i] ) {
			delete emitters[i];
			emitters[i] = NULL;
		}
	}
	localSound = NULL;
}

/*
===================
idSoundWorldLocal::ClearAllSoundEmitters
===================
*/
void idSoundWorldLocal::ClearAllSoundEmitters() {
	int i;

	Sys_EnterCriticalSection();

	AVIClose();

	for ( i = 0; i < emitters.Num(); i++ ) {
		idSoundEmitterLocal *sound = emitters[i];
		sound->Clear();
	}
	localSound = NULL;

	Sys_LeaveCriticalSection();
}

/*
===================
idSoundWorldLocal::AllocLocalSoundEmitter
===================
*/
idSoundEmitterLocal *idSoundWorldLocal::AllocLocalSoundEmitter() {
	int i, index;
	idSoundEmitterLocal *def = NULL;

	index = -1;

	// never use the 0 index spot

	for ( i = 1 ; i < emitters.Num() ; i++ ) {
		def = emitters[i];

		// check for a completed and freed spot
		if ( def->removeStatus >= REMOVE_STATUS_SAMPLEFINISHED ) {
			index = i;
			if ( idSoundSystemLocal::s_showStartSound.GetInteger() ) {
				common->Printf( "sound: recycling sound def %d\n", i );
			}
			break;
		}
	}

	if ( index == -1 ) {
		// append a brand new one
		def = new idSoundEmitterLocal;

		// we need to protect this from the async thread
		Sys_EnterCriticalSection();
		index = emitters.Append( def );
		Sys_LeaveCriticalSection();

		if ( idSoundSystemLocal::s_showStartSound.GetInteger() ) {
			common->Printf( "sound: appended new sound def %d\n", index );
		}
	}

	def->Clear();
	def->index = index;
	def->removeStatus = REMOVE_STATUS_ALIVE;
	def->soundWorld = this;

	return def;
}

/*
===================
idSoundWorldLocal::AllocSoundEmitter

  this is called from the main thread
===================
*/
idSoundEmitter *idSoundWorldLocal::AllocSoundEmitter() {
	idSoundEmitterLocal *emitter = AllocLocalSoundEmitter();

	if ( idSoundSystemLocal::s_showStartSound.GetInteger() ) {
		common->Printf( "AllocSoundEmitter = %i\n",  emitter->index );
	}
	if ( writeDemo ) {
		writeDemo->WriteInt( DS_SOUND );
		writeDemo->WriteInt( SCMD_ALLOC_EMITTER );
		writeDemo->WriteInt( emitter->index );
	}

	return emitter;
}

/*
===================
idSoundWorldLocal::StartWritingDemo

  this is called from the main thread
===================
*/
void idSoundWorldLocal::StartWritingDemo( idDemoFile *demo ) {
	writeDemo = demo;

	writeDemo->WriteInt( DS_SOUND );
	writeDemo->WriteInt( SCMD_STATE );

	// use the normal save game code to archive all the emitters
	WriteToSaveGame( writeDemo );
}

/*
===================
idSoundWorldLocal::StopWritingDemo

  this is called from the main thread
===================
*/
void idSoundWorldLocal::StopWritingDemo() {
	writeDemo = NULL;
}

/*
===================
idSoundWorldLocal::ProcessDemoCommand

  this is called from the main thread
===================
*/
void idSoundWorldLocal::ProcessDemoCommand( idDemoFile *readDemo ) {
	int	index;
	idSoundEmitterLocal	*def;

	if ( !readDemo ) {
		return;
	}

	int dc;

	if ( !readDemo->ReadInt( dc ) ) {
		return;
	}

	switch( (soundDemoCommand_t)dc ) {
	case SCMD_STATE:
		// we need to protect this from the async thread
		// other instances of calling idSoundWorldLocal::ReadFromSaveGame do this while the sound code is muted
		// setting muted and going right in may not be good enough here, as we async thread may already be in an async tick (in which case we could still race to it)
		Sys_EnterCriticalSection();
		ReadFromSaveGame( readDemo );
		Sys_LeaveCriticalSection();
		UnPause();
		break;
	case SCMD_PLACE_LISTENER:
		{
			idVec3	origin;
			idMat3	axis;
			int		listenerId;
			int		gameTime;

			readDemo->ReadVec3( origin );
			readDemo->ReadMat3( axis );
			readDemo->ReadInt( listenerId );
			readDemo->ReadInt( gameTime );

			PlaceListener( origin, axis, listenerId, gameTime, "" );
		};
		break;
	case SCMD_ALLOC_EMITTER:
		readDemo->ReadInt( index );
		if ( index < 1 || index > emitters.Num() ) {
			common->Error( "idSoundWorldLocal::ProcessDemoCommand: bad emitter number" );
		}
		if ( index == emitters.Num() ) {
			// append a brand new one
			def = new idSoundEmitterLocal;
			emitters.Append( def );
		}
		def = emitters[ index ];
		def->Clear();
		def->index = index;
		def->removeStatus = REMOVE_STATUS_ALIVE;
		def->soundWorld = this;
		break;
	case SCMD_FREE:
		{
			int	immediate;

			readDemo->ReadInt( index );
			readDemo->ReadInt( immediate );
			EmitterForIndex( index )->Free( immediate != 0 );
		}
		break;
	case SCMD_UPDATE:
		{
			idVec3 origin;
			int listenerId;
			soundShaderParms_t parms;

			readDemo->ReadInt( index );
			readDemo->ReadVec3( origin );
			readDemo->ReadInt( listenerId );
			readDemo->ReadFloat( parms.minDistance );
			readDemo->ReadFloat( parms.maxDistance );
			readDemo->ReadFloat( parms.volume );
			readDemo->ReadFloat( parms.shakes );
			readDemo->ReadInt( parms.soundShaderFlags );
			readDemo->ReadInt( parms.soundClass );
			EmitterForIndex( index )->UpdateEmitter( origin, listenerId, &parms );
		}
		break;
	case SCMD_START:
		{
			const idSoundShader *shader;
			int			channel;
			float		diversity;
			int			shaderFlags;

			readDemo->ReadInt( index );
			shader = declManager->FindSound( readDemo->ReadHashString() );
			readDemo->ReadInt( channel );
			readDemo->ReadFloat( diversity );
			readDemo->ReadInt( shaderFlags );
			EmitterForIndex( index )->StartSound( shader, (s_channelType)channel, diversity, shaderFlags );
		}
		break;
	case SCMD_MODIFY:
		{
			int		channel;
			soundShaderParms_t parms;

			readDemo->ReadInt( index );
			readDemo->ReadInt( channel );
			readDemo->ReadFloat( parms.minDistance );
			readDemo->ReadFloat( parms.maxDistance );
			readDemo->ReadFloat( parms.volume );
			readDemo->ReadFloat( parms.shakes );
			readDemo->ReadInt( parms.soundShaderFlags );
			readDemo->ReadInt( parms.soundClass );
			EmitterForIndex( index )->ModifySound( (s_channelType)channel, &parms );
		}
		break;
	case SCMD_STOP:
		{
			int		channel;

			readDemo->ReadInt( index );
			readDemo->ReadInt( channel );
			EmitterForIndex( index )->StopSound( (s_channelType)channel );
		}
		break;
	case SCMD_FADE:
		{
			int		channel;
			float	to, over;

			readDemo->ReadInt( index );
			readDemo->ReadInt( channel );
			readDemo->ReadFloat( to );
			readDemo->ReadFloat( over );
			EmitterForIndex( index )->FadeSound((s_channelType)channel, to, over );
		}
		break;
	}
}

/*
===================
idSoundWorldLocal::CurrentShakeAmplitudeForPosition

  this is called from the main thread
===================
*/
float idSoundWorldLocal::CurrentShakeAmplitudeForPosition( const int time, const idVec3 &listererPosition ) {
	float amp = 0.0f;
	int localTime;

	if ( idSoundSystemLocal::s_constantAmplitude.GetFloat() >= 0.0f ) {
		return 0.0f;
	}

	localTime = soundSystemLocal.GetCurrent44kHzTime();

	for ( int i = 1; i < emitters.Num(); i++ ) {
		idSoundEmitterLocal *sound = emitters[i];
		if ( !sound->hasShakes ) {
			continue;
		}
		amp += FindAmplitude( sound, localTime, &listererPosition, SCHANNEL_ANY, true );
	}
	return amp;
}

/*
===================
idSoundWorldLocal::MixLoop

Sum all sound contributions into finalMixBuffer, an unclamped float buffer holding
all output channels.  MIXBUFFER_SAMPLES samples will be created, with each sample consisting
of 2 or 6 floats depending on numSpeakers.

this is normally called from the sound thread, but also from the main thread
for AVIdemo writing
===================
*/
void idSoundWorldLocal::MixLoop( int current44kHz, int numSpeakers, float *finalMixBuffer ) {
	int i, j;
	idSoundEmitterLocal *sound;

	// if noclip flying outside the world, leave silence
	if ( listenerArea == -1 ) {
		alListenerf( AL_GAIN, 0.0f );
		return;
	}

	// update the listener position and orientation
	ALfloat listenerPosition[3];

	listenerPosition[0] = -listenerPos.y;
	listenerPosition[1] =  listenerPos.z;
	listenerPosition[2] = -listenerPos.x;

	ALfloat listenerOrientation[6];

	listenerOrientation[0] = -listenerAxis[0].y;
	listenerOrientation[1] =  listenerAxis[0].z;
	listenerOrientation[2] = -listenerAxis[0].x;

	listenerOrientation[3] = -listenerAxis[2].y;
	listenerOrientation[4] =  listenerAxis[2].z;
	listenerOrientation[5] = -listenerAxis[2].x;

	alListenerf( AL_GAIN, 1.0f );
	alListenerfv( AL_POSITION, listenerPosition );
	alListenerfv( AL_ORIENTATION, listenerOrientation );

	if (idSoundSystemLocal::useEFXReverb && soundSystemLocal.efxloaded) {
		ALuint effect = 0;
		idStr s(listenerArea);

		bool found = soundSystemLocal.EFXDatabase.FindEffect(s, &effect);
		if (!found) {
			s = listenerAreaName;
			found = soundSystemLocal.EFXDatabase.FindEffect(s, &effect);
		}
		if (!found) {
			s = "default";
			found = soundSystemLocal.EFXDatabase.FindEffect(s, &effect);
		}

		// only update if change in settings
		if (found && listenerEffect != effect) {
			EFXprintf("Switching to EFX '%s' (#%u)\n", s.c_str(), effect);
			listenerEffect = effect;
			soundSystemLocal.alAuxiliaryEffectSloti(listenerSlot, AL_EFFECTSLOT_EFFECT, effect);
		}
	}

	// debugging option to mute all but a single soundEmitter
	if ( idSoundSystemLocal::s_singleEmitter.GetInteger() > 0 && idSoundSystemLocal::s_singleEmitter.GetInteger() < emitters.Num() ) {
		sound = emitters[idSoundSystemLocal::s_singleEmitter.GetInteger()];

		if ( sound && sound->playing ) {
			// run through all the channels
			for ( j = 0; j < SOUND_MAX_CHANNELS ; j++ ) {
				idSoundChannel	*chan = &sound->channels[j];

				// see if we have a sound triggered on this channel
				if ( !chan->triggerState ) {
					chan->ALStop();
					continue;
				}

				AddChannelContribution( sound, chan, current44kHz, numSpeakers, finalMixBuffer );
			}
		}
		return;
	}

	for ( i = 1; i < emitters.Num(); i++ ) {
		sound = emitters[i];

		if ( !sound ) {
			continue;
		}
		// if no channels are active, do nothing
		if ( !sound->playing ) {
			continue;
		}
		// run through all the channels
		for ( j = 0; j < SOUND_MAX_CHANNELS ; j++ ) {
			idSoundChannel	*chan = &sound->channels[j];

			// see if we have a sound triggered on this channel
			if ( !chan->triggerState ) {
				chan->ALStop();
				continue;
			}

			AddChannelContribution( sound, chan, current44kHz, numSpeakers, finalMixBuffer );
		}
	}

	// TODO port to OpenAL
	if ( false && enviroSuitActive ) {
		soundSystemLocal.DoEnviroSuit( finalMixBuffer, MIXBUFFER_SAMPLES, numSpeakers );
	}
}

//==============================================================================

/*
===================
idSoundWorldLocal::AVIOpen

	this is called by the main thread
===================
*/
void idSoundWorldLocal::AVIOpen( const char *path, const char *name ) {
	aviDemoPath = path;
	aviDemoName = name;

	lastAVI44kHz = game44kHz - game44kHz % MIXBUFFER_SAMPLES;

	if ( idSoundSystemLocal::s_numberOfSpeakers.GetInteger() == 6 ) {
		fpa[0] = fileSystem->OpenFileWrite( aviDemoPath + "channel_51_left.raw" );
		fpa[1] = fileSystem->OpenFileWrite( aviDemoPath + "channel_51_right.raw" );
		fpa[2] = fileSystem->OpenFileWrite( aviDemoPath + "channel_51_center.raw" );
		fpa[3] = fileSystem->OpenFileWrite( aviDemoPath + "channel_51_lfe.raw" );
		fpa[4] = fileSystem->OpenFileWrite( aviDemoPath + "channel_51_backleft.raw" );
		fpa[5] = fileSystem->OpenFileWrite( aviDemoPath + "channel_51_backright.raw" );
	} else {
		fpa[0] = fileSystem->OpenFileWrite( aviDemoPath + "channel_left.raw" );
		fpa[1] = fileSystem->OpenFileWrite( aviDemoPath + "channel_right.raw" );
	}

	soundSystemLocal.SetMute( true );
}

/*
===================
idSoundWorldLocal::AVIUpdate

this is called by the main thread
writes one block of sound samples if enough time has passed
This can be used to write wave files even if no sound hardware exists
===================
*/
void idSoundWorldLocal::AVIUpdate() {
	int		numSpeakers;

	if ( game44kHz - lastAVI44kHz < MIXBUFFER_SAMPLES ) {
		return;
	}

	numSpeakers = idSoundSystemLocal::s_numberOfSpeakers.GetInteger();

	float	mix[MIXBUFFER_SAMPLES*6+16];
	float	*mix_p = (float *)((( intptr_t)mix + 15 ) & ~15);	// SIMD align

	SIMDProcessor->Memset( mix_p, 0, MIXBUFFER_SAMPLES*sizeof(float)*numSpeakers );

	MixLoop( lastAVI44kHz, numSpeakers, mix_p );

	for ( int i = 0; i < numSpeakers; i++ ) {
		short outD[MIXBUFFER_SAMPLES];

		for( int j = 0; j < MIXBUFFER_SAMPLES; j++ ) {
			float s = mix_p[ j*numSpeakers + i];
			if ( s < -32768.0f ) {
				outD[j] = -32768;
			} else if ( s > 32767.0f ) {
				outD[j] = 32767;
			} else {
				outD[j] = idMath::FtoiFast( s );
			}
		}
		// write to file
		fpa[i]->Write( outD, MIXBUFFER_SAMPLES*sizeof(short) );
	}

	lastAVI44kHz += MIXBUFFER_SAMPLES;

	return;
}

/*
===================
idSoundWorldLocal::AVIClose
===================
*/
void idSoundWorldLocal::AVIClose( void ) {
	int i;

	if ( !fpa[0] ) {
		return;
	}

	// make sure the final block is written
	game44kHz += MIXBUFFER_SAMPLES;
	AVIUpdate();
	game44kHz -= MIXBUFFER_SAMPLES;

	for ( i = 0; i < 6; i++ ) {
		if ( fpa[i] != NULL ) {
			fileSystem->CloseFile( fpa[i] );
			fpa[i] = NULL;
		}
	}
	if ( idSoundSystemLocal::s_numberOfSpeakers.GetInteger() == 2 ) {
		// convert it to a wave file
		idFile *rL, *lL, *wO;
		idStr	name;

		name = aviDemoPath + aviDemoName + ".wav";
		wO = fileSystem->OpenFileWrite( name );
		if ( !wO ) {
			common->Error( "Couldn't write %s", name.c_str() );
		}

		name = aviDemoPath + "channel_right.raw";
		rL = fileSystem->OpenFileRead( name );
		if ( !rL ) {
			common->Error( "Couldn't open %s", name.c_str() );
		}

		name = aviDemoPath + "channel_left.raw";
		lL = fileSystem->OpenFileRead( name );
		if ( !lL ) {
			common->Error( "Couldn't open %s", name.c_str() );
		}

		int numSamples = rL->Length()/2;
		mminfo_t	info;
		pcmwaveformat_t format;

		info.ckid = fourcc_riff;
		info.fccType = mmioFOURCC( 'W', 'A', 'V', 'E' );
		info.cksize = (rL->Length()*2) - 8 + 4 + 16 + 8 + 8;
		info.dwDataOffset = 12;

		wO->Write( &info, 12 );

		info.ckid = mmioFOURCC( 'f', 'm', 't', ' ' );
		info.cksize = 16;

		wO->Write( &info, 8 );

		format.wBitsPerSample = 16;
		format.wf.nAvgBytesPerSec = 44100*4;		// sample rate * block align
		format.wf.nChannels = 2;
		format.wf.nSamplesPerSec = 44100;
		format.wf.wFormatTag = WAVE_FORMAT_TAG_PCM;
		format.wf.nBlockAlign = 4;					// channels * bits/sample / 8

		wO->Write( &format, 16 );

		info.ckid = mmioFOURCC( 'd', 'a', 't', 'a' );
		info.cksize = rL->Length() * 2;

		wO->Write( &info, 8 );

		short s0, s1;
		for( i = 0; i < numSamples; i++ ) {
			lL->Read( &s0, 2 );
			rL->Read( &s1, 2 );
			wO->Write( &s0, 2 );
			wO->Write( &s1, 2 );
		}

		fileSystem->CloseFile( wO );
		fileSystem->CloseFile( lL );
		fileSystem->CloseFile( rL );

		fileSystem->RemoveFile( aviDemoPath + "channel_right.raw" );
		fileSystem->RemoveFile( aviDemoPath + "channel_left.raw" );
	}

	soundSystemLocal.SetMute( false );
}

//==============================================================================


/*
===================
idSoundWorldLocal::ResolveOrigin

Find out of the sound is completely occluded by a closed door portal, or
the virtual sound origin position at the portal closest to the listener.
  this is called by the main thread

dist is the distance from the orignial sound origin to the current portal that enters soundArea
def->distance is the distance we are trying to reduce.

If there is no path through open portals from the sound to the listener, def->distance will remain
set at maxDistance
===================
*/
static const int MAX_PORTAL_TRACE_DEPTH = 10;

void idSoundWorldLocal::ResolveOrigin( const int stackDepth, const soundPortalTrace_t *prevStack, const int soundArea, const float dist, const idVec3& soundOrigin, idSoundEmitterLocal *def ) {

	if ( dist >= def->distance ) {
		// we can't possibly hear the sound through this chain of portals
		return;
	}

	if ( soundArea == listenerArea ) {
		float	fullDist = dist + (soundOrigin - listenerQU).LengthFast();
		if ( fullDist < def->distance ) {
			def->distance = fullDist;
			def->spatializedOrigin = soundOrigin;
		}
		return;
	}

	if ( stackDepth == MAX_PORTAL_TRACE_DEPTH ) {
		// don't spend too much time doing these calculations in big maps
		return;
	}

	soundPortalTrace_t newStack;
	newStack.portalArea = soundArea;
	newStack.prevStack = prevStack;

	int numPortals = rw->NumPortalsInArea( soundArea );
	for( int p = 0; p < numPortals; p++ ) {
		exitPortal_t re = rw->GetPortal( soundArea, p );

		float	occlusionDistance = 0;

		// air blocking windows will block sound like closed doors
		if ( (re.blockingBits & ( PS_BLOCK_VIEW | PS_BLOCK_AIR ) ) ) {
			// we could just completely cut sound off, but reducing the volume works better
			// continue;
			occlusionDistance = idSoundSystemLocal::s_doorDistanceAdd.GetFloat();
		}

		// what area are we about to go look at
		int otherArea = re.areas[0];
		if ( re.areas[0] == soundArea ) {
			otherArea = re.areas[1];
		}

		// if this area is already in our portal chain, don't bother looking into it
		const soundPortalTrace_t *prev;
		for ( prev = prevStack ; prev ; prev = prev->prevStack ) {
			if ( prev->portalArea == otherArea ) {
				break;
			}
		}
		if ( prev ) {
			continue;
		}

		// pick a point on the portal to serve as our virtual sound origin
#if 1
		idVec3	source;

		idPlane	pl;
		re.w->GetPlane( pl );

		float	scale;
		idVec3	dir = listenerQU - soundOrigin;
		if ( !pl.RayIntersection( soundOrigin, dir, scale ) ) {
			source = re.w->GetCenter();
		} else {
			source = soundOrigin + scale * dir;

			// if this point isn't inside the portal edges, slide it in
			for ( int i = 0 ; i < re.w->GetNumPoints() ; i++ ) {
				int j = ( i + 1 ) % re.w->GetNumPoints();
				idVec3	edgeDir = (*(re.w))[j].ToVec3() - (*(re.w))[i].ToVec3();
				idVec3	edgeNormal;

				edgeNormal.Cross( pl.Normal(), edgeDir );

				idVec3	fromVert = source - (*(re.w))[j].ToVec3();

				float	d = edgeNormal * fromVert;
				if ( d > 0 ) {
					// move it in
					float div = edgeNormal.Normalize();
					d /= div;

					source -= d * edgeNormal;
				}
			}
		}
#else
		// clip the ray from the listener to the center of the portal by
		// all the portal edge planes, then project that point (or the original if not clipped)
		// onto the portal plane to get the spatialized origin

		idVec3	start = listenerQU;
		idVec3	mid = re.w->GetCenter();
		bool	wasClipped = false;

		for ( int i = 0 ; i < re.w->GetNumPoints() ; i++ ) {
			int j = ( i + 1 ) % re.w->GetNumPoints();
			idVec3	v1 = (*(re.w))[j].ToVec3() - soundOrigin;
			idVec3	v2 = (*(re.w))[i].ToVec3() - soundOrigin;

			v1.Normalize();
			v2.Normalize();

			idVec3	edgeNormal;

			edgeNormal.Cross( v1, v2 );

			idVec3	fromVert = start - soundOrigin;
			float	d1 = edgeNormal * fromVert;

			if ( d1 > 0.0f ) {
				fromVert = mid - (*(re.w))[j].ToVec3();
				float d2 = edgeNormal * fromVert;

				// move it in
				float	f = d1 / ( d1 - d2 );

				idVec3	clipped = start * ( 1.0f - f ) + mid * f;
				start = clipped;
				wasClipped = true;
			}
		}

		idVec3	source;
		if ( wasClipped ) {
			// now project it onto the portal plane
			idPlane	pl;
			re.w->GetPlane( pl );

			float	f1 = pl.Distance( start );
			float	f2 = pl.Distance( soundOrigin );

			float	f = f1 / ( f1 - f2 );
			source = start * ( 1.0f - f ) + soundOrigin * f;
		} else {
			source = soundOrigin;
		}
#endif

		idVec3 tlen = source - soundOrigin;
		float tlenLength = tlen.LengthFast();

		ResolveOrigin( stackDepth+1, &newStack, otherArea, dist+tlenLength+occlusionDistance, source, def );
	}
}


/*
===================
idSoundWorldLocal::PlaceListener

  this is called by the main thread
===================
*/
void idSoundWorldLocal::PlaceListener( const idVec3& origin, const idMat3& axis,
									const int listenerId, const int gameTime, const idStr& areaName  ) {

	int current44kHzTime;

	if ( !soundSystemLocal.isInitialized ) {
		return;
	}

	if ( pause44kHz >= 0 ){
		return;
	}

	if ( writeDemo ) {
		writeDemo->WriteInt( DS_SOUND );
		writeDemo->WriteInt( SCMD_PLACE_LISTENER );
		writeDemo->WriteVec3( origin );
		writeDemo->WriteMat3( axis );
		writeDemo->WriteInt( listenerId );
		writeDemo->WriteInt( gameTime );
	}

	current44kHzTime = soundSystemLocal.GetCurrent44kHzTime();

	// we usually expect gameTime to be increasing by 16 or 32 msec, but when
	// a cinematic is fast-forward skipped through, it can jump by a significant
	// amount, while the hardware 44kHz position will not have changed accordingly,
	// which would make sounds (like long character speaches) continue from the
	// old time.  Fix this by killing all non-looping sounds
	if ( gameTime > gameMsec + 500 ) {
		OffsetSoundTime( - ( gameTime - gameMsec ) * 0.001f * 44100.0f );
	}

	gameMsec = gameTime;
	if ( fpa[0] ) {
		// exactly 30 fps so the wave file can be used for exact video frames
		game44kHz = idMath::FtoiFast( gameMsec * ( ( 1000.0f / 60.0f ) / 16.0f ) * 0.001f * 44100.0f );
	} else {
		// the normal 16 msec / frame
		game44kHz = idMath::FtoiFast( gameMsec * 0.001f * 44100.0f );
	}

	listenerPrivateId = listenerId;

	listenerQU = origin;							// Doom units
	listenerPos = origin * DOOM_TO_METERS;			// meters
	listenerAxis = axis;
	listenerAreaName = areaName;
	listenerAreaName.ToLower();

	if ( rw ) {
		listenerArea = rw->PointInArea( listenerQU );	// where are we?
	} else {
		listenerArea = 0;
	}

	if ( listenerArea < 0 ) {
		return;
	}

	ForegroundUpdate( current44kHzTime );
}

/*
==================
idSoundWorldLocal::ForegroundUpdate
==================
*/
void idSoundWorldLocal::ForegroundUpdate( int current44kHzTime ) {
	int j, k;
	idSoundEmitterLocal	*def;

	if ( !soundSystemLocal.isInitialized ) {
		return;
	}

	Sys_EnterCriticalSection();

	// if we are recording an AVI demo, don't use hardware time
	if ( fpa[0] ) {
		current44kHzTime = lastAVI44kHz;
	}

	//
	// check to see if each sound is visible or not
	// speed up by checking maxdistance to origin
	// although the sound may still need to play if it has
	// just become occluded so it can ramp down to 0
	//
	for ( j = 1; j < emitters.Num(); j++ ) {
		def = emitters[j];

		if ( def->removeStatus >= REMOVE_STATUS_SAMPLEFINISHED ) {
			continue;
		}

		// see if our last channel just finished
		def->CheckForCompletion( current44kHzTime );

		if ( !def->playing ) {
			continue;
		}

		// update virtual origin / distance, etc
		def->Spatialize( listenerPos, listenerArea, rw );

		// per-sound debug options
		if ( idSoundSystemLocal::s_drawSounds.GetInteger() && rw ) {
			if ( def->distance < def->maxDistance || idSoundSystemLocal::s_drawSounds.GetInteger() > 1 ) {
				idBounds ref;
				ref.Clear();
				ref.AddPoint( idVec3( -10, -10, -10 ) );
				ref.AddPoint( idVec3(  10,  10,  10 ) );
				float vis = (1.0f - (def->distance / def->maxDistance));

				// draw a box
				rw->DebugBounds( idVec4( vis, 0.25f, vis, vis ), ref, def->origin );

				// draw an arrow to the audible position, possible a portal center
				if ( def->origin != def->spatializedOrigin ) {
					rw->DebugArrow( colorRed, def->origin, def->spatializedOrigin, 4 );
				}

				// draw the index
				idVec3	textPos = def->origin;
				textPos[2] -= 8;
				rw->DrawText( va("%i", def->index), textPos, 0.1f, idVec4(1,0,0,1), listenerAxis );
				textPos[2] += 8;

				// run through all the channels
				for ( k = 0; k < SOUND_MAX_CHANNELS ; k++ ) {
					idSoundChannel	*chan = &def->channels[k];

					// see if we have a sound triggered on this channel
					if ( !chan->triggerState ) {
						continue;
					}

					char	text[1024];
					float	min = chan->parms.minDistance;
					float	max = chan->parms.maxDistance;
					const char	*defaulted = chan->leadinSample->defaultSound ? "(DEFAULTED)" : "";
					sprintf( text, "%s (%i/%i %i/%i)%s", chan->soundShader->GetName(), (int)def->distance,
						(int)def->realDistance, (int)min, (int)max, defaulted );
					rw->DrawText( text, textPos, 0.1f, idVec4(1,0,0,1), listenerAxis );
					textPos[2] += 8;
				}
			}
		}
	}

	Sys_LeaveCriticalSection();

	//
	// the sound meter
	//
	if ( idSoundSystemLocal::s_showLevelMeter.GetInteger() ) {
		const idMaterial *gui = declManager->FindMaterial( "guis/assets/soundmeter/audiobg", false );
		if ( gui ) {
			const shaderStage_t *foo = gui->GetStage(0);
			if ( !foo->texture.cinematic ) {
				((shaderStage_t *)foo)->texture.cinematic = new idSndWindow;
			}
		}
	}

	//
	// optionally dump out the generated sound
	//
	if ( fpa[0] ) {
		AVIUpdate();
	}
}

/*
===================
idSoundWorldLocal::OffsetSoundTime
===================
*/
void idSoundWorldLocal::OffsetSoundTime( int offset44kHz ) {
	int i, j;

	for ( i = 0; i < emitters.Num(); i++ ) {
		if ( emitters[i] == NULL ) {
			continue;
		}
		for ( j = 0; j < SOUND_MAX_CHANNELS; j++ ) {
			idSoundChannel *chan = &emitters[i]->channels[ j ];

			if ( !chan->triggerState ) {
				continue;
			}

			chan->trigger44kHzTime += offset44kHz;
		}
	}
}

/*
===================
idSoundWorldLocal::WriteToSaveGame
===================
*/
void idSoundWorldLocal::WriteToSaveGame( idFile *savefile ) {
	int i, j, num, currentSoundTime;
	const char *name;

	// the game soundworld is always paused at this point, save that time down
	if ( pause44kHz > 0 ) {
		currentSoundTime = pause44kHz;
	} else {
		currentSoundTime = soundSystemLocal.GetCurrent44kHzTime();
	}

	// write listener data
	savefile->WriteVec3(listenerQU);
	savefile->WriteMat3(listenerAxis);
	savefile->WriteInt(listenerPrivateId);
	savefile->WriteInt(gameMsec);
	savefile->WriteInt(game44kHz);
	savefile->WriteInt(currentSoundTime);

	num = emitters.Num();
	savefile->WriteInt(num);

	for ( i = 1; i < emitters.Num(); i++ ) {
		idSoundEmitterLocal *def = emitters[i];

		if ( def->removeStatus != REMOVE_STATUS_ALIVE ) {
			int skip = -1;
			savefile->Write( &skip, sizeof( skip ) );
			continue;
		}

		savefile->WriteInt(i);

		// Write the emitter data
		savefile->WriteVec3( def->origin );
		savefile->WriteInt( def->listenerId );
		WriteToSaveGameSoundShaderParams( savefile, &def->parms );
		savefile->WriteFloat( def->amplitude );
		savefile->WriteInt( def->ampTime );
		for (int k = 0; k < SOUND_MAX_CHANNELS; k++)
			WriteToSaveGameSoundChannel( savefile, &def->channels[k] );
		savefile->WriteFloat( def->distance );
		savefile->WriteBool( def->hasShakes );
		savefile->WriteInt( def->lastValidPortalArea );
		savefile->WriteFloat( def->maxDistance );
		savefile->WriteBool( def->playing );
		savefile->WriteFloat( def->realDistance );
		savefile->WriteInt( def->removeStatus );
		savefile->WriteVec3( def->spatializedOrigin );

		// write the channel data
		for( j = 0; j < SOUND_MAX_CHANNELS; j++ ) {
			idSoundChannel *chan = &def->channels[ j ];

			// Write out any sound commands for this def
			if ( chan->triggerState && chan->soundShader && chan->leadinSample ) {

				savefile->WriteInt( j );

				// write the pointers out separately
				name = chan->soundShader->GetName();
				savefile->WriteString( name );

				name = chan->leadinSample->name;
				savefile->WriteString( name );
			}
		}

		// End active channels with -1
		int end = -1;
		savefile->WriteInt( end );
	}

	// new in Doom3 v1.2
	savefile->Write( &slowmoActive, sizeof( slowmoActive ) );
	savefile->Write( &slowmoSpeed, sizeof( slowmoSpeed ) );
	savefile->Write( &enviroSuitActive, sizeof( enviroSuitActive ) );
}

/*
 ===================
 idSoundWorldLocal::WriteToSaveGameSoundShaderParams
 ===================
 */
void idSoundWorldLocal::WriteToSaveGameSoundShaderParams( idFile *saveGame, soundShaderParms_t *params ) {
	saveGame->WriteFloat(params->minDistance);
	saveGame->WriteFloat(params->maxDistance);
	saveGame->WriteFloat(params->volume);
	saveGame->WriteFloat(params->shakes);
	saveGame->WriteInt(params->soundShaderFlags);
	saveGame->WriteInt(params->soundClass);
}

/*
 ===================
 idSoundWorldLocal::WriteToSaveGameSoundChannel
 ===================
 */
void idSoundWorldLocal::WriteToSaveGameSoundChannel( idFile *saveGame, idSoundChannel *ch ) {
	saveGame->WriteBool( ch->triggerState );
	saveGame->WriteUnsignedChar( 0 );
	saveGame->WriteUnsignedChar( 0 );
	saveGame->WriteUnsignedChar( 0 );
	saveGame->WriteInt( ch->trigger44kHzTime );
	saveGame->WriteInt( ch->triggerGame44kHzTime );
	WriteToSaveGameSoundShaderParams( saveGame, &ch->parms );
	saveGame->WriteInt( 0 /* ch->leadinSample */ );
	saveGame->WriteInt( ch->triggerChannel );
	saveGame->WriteInt( 0 /* ch->soundShader */ );
	saveGame->WriteInt( 0 /* ch->decoder */ );
	saveGame->WriteFloat(ch->diversity );
	saveGame->WriteFloat(ch->lastVolume );
	for (int m = 0; m < 6; m++)
		saveGame->WriteFloat( ch->lastV[m] );
	saveGame->WriteInt( ch->channelFade.fadeStart44kHz );
	saveGame->WriteInt( ch->channelFade.fadeEnd44kHz );
	saveGame->WriteFloat( ch->channelFade.fadeStartVolume );
	saveGame->WriteFloat( ch->channelFade.fadeEndVolume );
}

/*
===================
idSoundWorldLocal::ReadFromSaveGame
===================
*/
void idSoundWorldLocal::ReadFromSaveGame( idFile *savefile ) {
	int i, num, handle, listenerId, gameTime, channel;
	int savedSoundTime, currentSoundTime, soundTimeOffset;
	idSoundEmitterLocal *def;
	idVec3 origin;
	idMat3 axis;
	idStr soundShader;

	ClearAllSoundEmitters();

	savefile->ReadVec3( origin );
	savefile->ReadMat3( axis );
	savefile->ReadInt( listenerId );
	savefile->ReadInt( gameTime );
	savefile->ReadInt( game44kHz );
	savefile->ReadInt( savedSoundTime );

	// we will adjust the sound starting times from those saved with the demo
	currentSoundTime = soundSystemLocal.GetCurrent44kHzTime();
	soundTimeOffset = currentSoundTime - savedSoundTime;

	// at the end of the level load we unpause the sound world and adjust the sound starting times once more
	pause44kHz = currentSoundTime;

	// place listener
	PlaceListener( origin, axis, listenerId, gameTime, "Undefined" );

	// make sure there are enough
	// slots to read the saveGame in.  We don't shrink the list
	// if there are extras.
	savefile->ReadInt( num );

	while( emitters.Num() < num ) {
		def = new idSoundEmitterLocal;
		def->index = emitters.Append( def );
		def->soundWorld = this;
	}

	// read in the state
	for ( i = 1; i < num; i++ ) {

		savefile->ReadInt( handle );
		if ( handle < 0 ) {
			continue;
		}
		if ( handle != i ) {
			common->Error( "idSoundWorldLocal::ReadFromSaveGame: index mismatch" );
		}
		def = emitters[i];

		def->removeStatus = REMOVE_STATUS_ALIVE;
		def->playing = true;		// may be reset by the first UpdateListener

		savefile->ReadVec3( def->origin );
		savefile->ReadInt( def->listenerId );
		ReadFromSaveGameSoundShaderParams( savefile, &def->parms );
		savefile->ReadFloat( def->amplitude );
		savefile->ReadInt( def->ampTime );
		for (int k = 0; k < SOUND_MAX_CHANNELS; k++)
			ReadFromSaveGameSoundChannel( savefile, &def->channels[k] );
		savefile->ReadFloat( def->distance );
		savefile->ReadBool( def->hasShakes );
		savefile->ReadInt( def->lastValidPortalArea );
		savefile->ReadFloat( def->maxDistance );
		savefile->ReadBool( def->playing );
		savefile->ReadFloat( def->realDistance );
		savefile->ReadInt( (int&)def->removeStatus );
		savefile->ReadVec3( def->spatializedOrigin );

		// read the individual channels
		savefile->ReadInt( channel );

		while ( channel >= 0 ) {
			if ( channel > SOUND_MAX_CHANNELS ) {
				common->Error( "idSoundWorldLocal::ReadFromSaveGame: channel > SOUND_MAX_CHANNELS" );
			}

			idSoundChannel *chan = &def->channels[channel];

			if ( !chan->decoder ) {
				// The pointer in the save file is not valid, so we grab a new one
				chan->decoder = idSampleDecoder::Alloc();
			}

			savefile->ReadString( soundShader );
			chan->soundShader = declManager->FindSound( soundShader );

			savefile->ReadString( soundShader );
			// load savegames with s_noSound 1
			if ( soundSystemLocal.soundCache ) {
				chan->leadinSample = soundSystemLocal.soundCache->FindSound( soundShader, false );
			} else {
				chan->leadinSample = NULL;
			}

			// adjust the hardware start time
			chan->trigger44kHzTime += soundTimeOffset;

			// make sure we start up the hardware voice if needed
			chan->triggered = chan->triggerState;
			chan->openalStreamingOffset = currentSoundTime - chan->trigger44kHzTime;

			// adjust the hardware fade time
			if ( chan->channelFade.fadeStart44kHz != 0 ) {
				chan->channelFade.fadeStart44kHz += soundTimeOffset;
				chan->channelFade.fadeEnd44kHz += soundTimeOffset;
			}

			// next command
			savefile->ReadInt( channel );
		}
	}

	if ( session->GetSaveGameVersion() >= 17 ) {
		savefile->Read( &slowmoActive, sizeof( slowmoActive ) );
		savefile->Read( &slowmoSpeed, sizeof( slowmoSpeed ) );
		savefile->Read( &enviroSuitActive, sizeof( enviroSuitActive ) );
	} else {
		slowmoActive		= false;
		slowmoSpeed			= 0;
		enviroSuitActive	= false;
	}
}

/*
 ===================
 idSoundWorldLocal::ReadFromSaveGameSoundShaderParams
 ===================
 */
void idSoundWorldLocal::ReadFromSaveGameSoundShaderParams( idFile *saveGame, soundShaderParms_t *params ) {
	saveGame->ReadFloat(params->minDistance);
	saveGame->ReadFloat(params->maxDistance);
	saveGame->ReadFloat(params->volume);
	saveGame->ReadFloat(params->shakes);
	saveGame->ReadInt(params->soundShaderFlags);
	saveGame->ReadInt(params->soundClass);
}

/*
 ===================
 idSoundWorldLocal::ReadFromSaveGameSoundChannel
 ===================
 */
void idSoundWorldLocal::ReadFromSaveGameSoundChannel( idFile *saveGame, idSoundChannel *ch ) {
	saveGame->ReadBool( ch->triggerState );
	char tmp;
	int i;
	saveGame->ReadChar( tmp );
	saveGame->ReadChar( tmp );
	saveGame->ReadChar( tmp );
	saveGame->ReadInt( ch->trigger44kHzTime );
	saveGame->ReadInt( ch->triggerGame44kHzTime );
	ReadFromSaveGameSoundShaderParams( saveGame, &ch->parms );
	saveGame->ReadInt( i );
	ch->leadinSample = NULL;
	saveGame->ReadInt( ch->triggerChannel );
	saveGame->ReadInt( i );
	ch->soundShader = NULL;
	saveGame->ReadInt( i );
	ch->decoder = NULL;
	saveGame->ReadFloat(ch->diversity );
	saveGame->ReadFloat(ch->lastVolume );
	for (int m = 0; m < 6; m++)
		saveGame->ReadFloat( ch->lastV[m] );
	saveGame->ReadInt( ch->channelFade.fadeStart44kHz );
	saveGame->ReadInt( ch->channelFade.fadeEnd44kHz );
	saveGame->ReadFloat( ch->channelFade.fadeStartVolume );
	saveGame->ReadFloat( ch->channelFade.fadeEndVolume );
}

/*
===================
idSoundWorldLocal::EmitterForIndex
===================
*/
idSoundEmitter	*idSoundWorldLocal::EmitterForIndex( int index ) {
	if ( index == 0 ) {
		return NULL;
	}
	if ( index >= emitters.Num() ) {
		common->Error( "idSoundWorldLocal::EmitterForIndex: %i > %i", index, emitters.Num() );
	}
	return emitters[index];
}

/*
===============
idSoundWorldLocal::StopAllSounds

  this is called from the main thread
===============
*/
void idSoundWorldLocal::StopAllSounds() {

	for ( int i = 0; i < emitters.Num(); i++ ) {
		idSoundEmitterLocal * def = emitters[i];
		def->StopSound( SCHANNEL_ANY );
	}
}

/*
===============
idSoundWorldLocal::Pause
===============
*/
void idSoundWorldLocal::Pause( void ) {
	if ( pause44kHz >= 0 ) {
		common->Warning( "idSoundWorldLocal::Pause: already paused" );
		return;
	}

	pause44kHz = soundSystemLocal.GetCurrent44kHzTime();
}

/*
===============
idSoundWorldLocal::UnPause
===============
*/
void idSoundWorldLocal::UnPause( void ) {
	int offset44kHz;

	if ( pause44kHz < 0 ) {
		common->Warning( "idSoundWorldLocal::UnPause: not paused" );
		return;
	}

	offset44kHz = soundSystemLocal.GetCurrent44kHzTime() - pause44kHz;
	OffsetSoundTime( offset44kHz );

	pause44kHz = -1;
}

/*
===============
idSoundWorldLocal::IsPaused
===============
*/
bool idSoundWorldLocal::IsPaused( void ) {
	return ( pause44kHz >= 0 );
}

/*
===============
idSoundWorldLocal::PlayShaderDirectly

start a music track

  this is called from the main thread
===============
*/
void idSoundWorldLocal::PlayShaderDirectly( const char *shaderName, int channel ) {

	if ( localSound && channel == -1 ) {
		localSound->StopSound( SCHANNEL_ANY );
	} else if ( localSound ) {
		localSound->StopSound( channel );
	}

	if ( !shaderName || !shaderName[0] ) {
		return;
	}

	const idSoundShader *shader = declManager->FindSound( shaderName );
	if ( !shader ) {
		return;
	}

	if ( !localSound ) {
		localSound = AllocLocalSoundEmitter();
	}

	static idRandom	rnd;
	float	diversity = rnd.RandomFloat();

	localSound->StartSound( shader, ( channel == -1 ) ? SCHANNEL_ONE : channel , diversity, SSF_GLOBAL );

	// in case we are at the console without a game doing updates, force an update
	ForegroundUpdate( soundSystemLocal.GetCurrent44kHzTime() );
}

/*
===============
idSoundWorldLocal::CalcEars

Determine the volumes from each speaker for a given sound emitter
===============
*/
void idSoundWorldLocal::CalcEars( int numSpeakers, idVec3 spatializedOrigin, idVec3 listenerPos,
								 idMat3 listenerAxis, float ears[6], float spatialize ) {
	idVec3 svec = spatializedOrigin - listenerPos;
	idVec3 ovec;

	ovec[0] = svec * listenerAxis[0];
	ovec[1] = svec * listenerAxis[1];
	ovec[2] = svec * listenerAxis[2];

	ovec.Normalize();

	if ( numSpeakers == 6 ) {
		static idVec3	speakerVector[6] = {
			idVec3(  0.707f,  0.707f, 0.0f ),	// front left
			idVec3(  0.707f, -0.707f, 0.0f ),	// front right
			idVec3(  0.707f,  0.0f,   0.0f ),	// front center
			idVec3(  0.0f,    0.0f,   0.0f ),	// sub
			idVec3( -0.707f,  0.707f, 0.0f ),	// rear left
			idVec3( -0.707f, -0.707f, 0.0f )	// rear right
		};
		for ( int i = 0 ; i < 6 ; i++ ) {
			if ( i == 3 ) {
				ears[i] = idSoundSystemLocal::s_subFraction.GetFloat();		// subwoofer
				continue;
			}
			float dot = ovec * speakerVector[i];
			ears[i] = (idSoundSystemLocal::s_dotbias6.GetFloat() + dot) / ( 1.0f + idSoundSystemLocal::s_dotbias6.GetFloat() );
			if ( ears[i] < idSoundSystemLocal::s_minVolume6.GetFloat() ) {
				ears[i] = idSoundSystemLocal::s_minVolume6.GetFloat();
			}
		}
	} else {
		float dot = ovec.y;
		float dotBias = idSoundSystemLocal::s_dotbias2.GetFloat();

		// when we are inside the minDistance, start reducing the amount of spatialization
		// so NPC voices right in front of us aren't quieter that off to the side
		dotBias += ( idSoundSystemLocal::s_spatializationDecay.GetFloat() - dotBias ) * ( 1.0f - spatialize );

		ears[0] = (idSoundSystemLocal::s_dotbias2.GetFloat() + dot) / ( 1.0f + dotBias );
		ears[1] = (idSoundSystemLocal::s_dotbias2.GetFloat() - dot) / ( 1.0f + dotBias );

		if ( ears[0] < idSoundSystemLocal::s_minVolume2.GetFloat() ) {
			ears[0] = idSoundSystemLocal::s_minVolume2.GetFloat();
		}
		if ( ears[1] < idSoundSystemLocal::s_minVolume2.GetFloat() ) {
			ears[1] = idSoundSystemLocal::s_minVolume2.GetFloat();
		}

		ears[2] =
		ears[3] =
		ears[4] =
		ears[5] = 0.0f;
	}
}

/*
===============
idSoundWorldLocal::AddChannelContribution

Adds the contribution of a single sound channel to finalMixBuffer
this is called from the async thread

Mixes MIXBUFFER_SAMPLES samples starting at current44kHz sample time into
finalMixBuffer
===============
*/
void idSoundWorldLocal::AddChannelContribution( idSoundEmitterLocal *sound, idSoundChannel *chan,
				   int current44kHz, int numSpeakers, float *finalMixBuffer ) {
	int j;
	float volume;

	//
	// get the sound definition and parameters from the entity
	//
	soundShaderParms_t *parms = &chan->parms;

	// assume we have a sound triggered on this channel
	assert( chan->triggerState );

	// fetch the actual wave file and see if it's valid
	idSoundSample *sample = chan->leadinSample;
	if ( sample == NULL ) {
		return;
	}

	// if you don't want to hear all the beeps from missing sounds
	if ( sample->defaultSound && !idSoundSystemLocal::s_playDefaultSound.GetBool() ) {
		return;
	}

	// get the actual shader
	const idSoundShader *shader = chan->soundShader;

	// this might happen if the foreground thread just deleted the sound emitter
	if ( !shader ) {
		return;
	}

	float maxd = parms->maxDistance;
	float mind = parms->minDistance;

	int  mask = shader->speakerMask;
	bool omni = ( parms->soundShaderFlags & SSF_OMNIDIRECTIONAL) != 0;
	bool looping = ( parms->soundShaderFlags & SSF_LOOPING ) != 0;
	bool global = ( parms->soundShaderFlags & SSF_GLOBAL ) != 0;
	bool noOcclusion = ( parms->soundShaderFlags & SSF_NO_OCCLUSION ) || !idSoundSystemLocal::s_useOcclusion.GetBool();

	// speed goes from 1 to 0.2
	if ( idSoundSystemLocal::s_slowAttenuate.GetBool() && slowmoActive && !chan->disallowSlow ) {
		maxd *= slowmoSpeed;
	}

	// stereo samples are always omni
	if ( sample->objectInfo.nChannels == 2 ) {
		omni = true;
	}

	// if the sound is playing from the current listener, it will not be spatialized at all
	if ( sound->listenerId == listenerPrivateId ) {
		global = true;
	}

	//
	// see if it's in range
	//

	// convert volumes from decibels to float scale

	// leadin volume scale for shattering lights
	// this isn't exactly correct, because the modified volume will get applied to
	// some initial chunk of the loop as well, because the volume is scaled for the
	// entire mix buffer
	if ( shader->leadinVolume && current44kHz - chan->trigger44kHzTime < sample->LengthIn44kHzSamples() ) {
		volume = soundSystemLocal.dB2Scale( shader->leadinVolume );
	} else {
		volume = soundSystemLocal.dB2Scale( parms->volume );
	}

	// global volume scale
	volume *= soundSystemLocal.dB2Scale( idSoundSystemLocal::s_volume.GetFloat() );

	// DG: scaling the volume of *everything* down a bit to prevent some sounds
	//     (like shotgun shot) being "drowned" when lots of other loud sounds
	//     (like shotgun impacts on metal) are played at the same time
	//     I guess this happens because the loud sounds mixed together are too loud so
	//     OpenAL just makes *everything* quiter or sth like that.
	//     See also https://github.com/dhewm/dhewm3/issues/179
	volume *= 0.333f; // (0.333 worked fine, 0.5 didn't)

	// volume fading
	float	fadeDb = chan->channelFade.FadeDbAt44kHz( current44kHz );
	volume *= soundSystemLocal.dB2Scale( fadeDb );

	fadeDb = soundClassFade[parms->soundClass].FadeDbAt44kHz( current44kHz );
	volume *= soundSystemLocal.dB2Scale( fadeDb );


	//
	// if it's a global sound then
	// it's not affected by distance or occlusion
	//
	float	spatialize = 1;
	idVec3 spatializedOriginInMeters;
	if ( !global ) {
		float	dlen;

		if ( noOcclusion ) {
			// use the real origin and distance
			spatializedOriginInMeters = sound->origin * DOOM_TO_METERS;
			dlen = sound->realDistance;
		} else {
			// use the possibly portal-occluded origin and distance
			spatializedOriginInMeters = sound->spatializedOrigin * DOOM_TO_METERS;
			dlen = sound->distance;
		}

		// reduce volume based on distance
		if ( dlen >= maxd ) {
			volume = 0.0f;
		} else if ( dlen > mind ) {
			float frac = idMath::ClampFloat( 0.0f, 1.0f, 1.0f - ((dlen - mind) / (maxd - mind)));
			if ( idSoundSystemLocal::s_quadraticFalloff.GetBool() ) {
				frac *= frac;
			}
			volume *= frac;
		} else if ( mind > 0.0f ) {
			// we tweak the spatialization bias when you are inside the minDistance
			spatialize = dlen / mind;
		}
	}

	//
	// if it is a private sound, set the volume to zero
	// unless we match the listenerId
	//
	if ( parms->soundShaderFlags & SSF_PRIVATE_SOUND ) {
		if ( sound->listenerId != listenerPrivateId ) {
			volume = 0;
		}
	}
	if ( parms->soundShaderFlags & SSF_ANTI_PRIVATE_SOUND ) {
		if ( sound->listenerId == listenerPrivateId ) {
			volume = 0;
		}
	}

	//
	// do we have anything to add?
	//
	if ( volume < SND_EPSILON && chan->lastVolume < SND_EPSILON ) {
		return;
	}
	chan->lastVolume = volume;

	//
	// fetch the sound from the cache as 44kHz, 16 bit samples
	//
	int offset = current44kHz - chan->trigger44kHzTime;
	float inputSamples[MIXBUFFER_SAMPLES*2+16];
	float *alignedInputSamples = (float *) ( ( ( (intptr_t)inputSamples ) + 15 ) & ~15 );

	//
	// allocate and initialize hardware source
	//
	if ( sound->removeStatus < REMOVE_STATUS_SAMPLEFINISHED ) {
		if ( !alIsSource( chan->openalSource ) ) {
			chan->openalSource = soundSystemLocal.AllocOpenALSource( chan, !chan->leadinSample->hardwareBuffer || !chan->soundShader->entries[0]->hardwareBuffer || looping, chan->leadinSample->objectInfo.nChannels == 2 );
		}

		if ( alIsSource( chan->openalSource ) ) {

			// stop source if needed..
			if ( chan->triggered ) {
				alSourceStop( chan->openalSource );
			}

			// update source parameters
			if ( global || omni ) {
				alSourcei( chan->openalSource, AL_SOURCE_RELATIVE, AL_TRUE);
				alSource3f( chan->openalSource, AL_POSITION, 0.0f, 0.0f, 0.0f );
				alSourcef( chan->openalSource, AL_GAIN, ( volume ) < ( 1.0f ) ? ( volume ) : ( 1.0f ) );
			} else {
				alSourcei( chan->openalSource, AL_SOURCE_RELATIVE, AL_FALSE);
				alSource3f( chan->openalSource, AL_POSITION, -spatializedOriginInMeters.y, spatializedOriginInMeters.z, -spatializedOriginInMeters.x );
				alSourcef( chan->openalSource, AL_GAIN, ( volume ) < ( 1.0f ) ? ( volume ) : ( 1.0f ) );
			}
			// DG: looping sounds with a leadin can't just use a HW buffer and openal's AL_LOOPING
			//     because we need to switch from leadin to the looped sound.. see https://github.com/dhewm/dhewm3/issues/291
			bool haveLeadin = chan->soundShader->numLeadins > 0;
			alSourcei( chan->openalSource, AL_LOOPING, ( looping && chan->soundShader->entries[0]->hardwareBuffer && !haveLeadin ) ? AL_TRUE : AL_FALSE );
#if 1
			alSourcef( chan->openalSource, AL_REFERENCE_DISTANCE, mind );
			alSourcef( chan->openalSource, AL_MAX_DISTANCE, maxd );
#endif
			alSourcef( chan->openalSource, AL_PITCH, ( slowmoActive && !chan->disallowSlow ) ? ( slowmoSpeed ) : ( 1.0f ) );

			if (idSoundSystemLocal::useEFXReverb) {
				if (enviroSuitActive) {
					alSourcei(chan->openalSource, AL_DIRECT_FILTER, listenerFilters[0]);
					alSource3i(chan->openalSource, AL_AUXILIARY_SEND_FILTER, listenerSlot, 0, listenerFilters[1]);
				} else {
					alSourcei(chan->openalSource, AL_DIRECT_FILTER, AL_FILTER_NULL);
					alSource3i(chan->openalSource, AL_AUXILIARY_SEND_FILTER, listenerSlot, 0, AL_FILTER_NULL);
				}
			}


			if ( ( !looping && chan->leadinSample->hardwareBuffer )
				|| ( looping && !haveLeadin && chan->soundShader->entries[0]->hardwareBuffer ) ) {
				// handle uncompressed (non streaming) single shot and looping sounds
				// DG: ... that have no leadin (with leadin we still need to switch to another sound,
				//     just use streaming code for that) - see https://github.com/dhewm/dhewm3/issues/291
				if ( chan->triggered ) {
					alSourcei( chan->openalSource, AL_BUFFER, looping ? chan->soundShader->entries[0]->openalBuffer : chan->leadinSample->openalBuffer );
				}
			} else {
				ALint finishedbuffers;
				ALuint buffers[3];

				// handle streaming sounds (decode on the fly) both single shot AND looping
				if ( chan->triggered ) {
					alSourcei( chan->openalSource, AL_BUFFER, 0 );
					alDeleteBuffers( 3, &chan->lastopenalStreamingBuffer[0] );
					chan->lastopenalStreamingBuffer[0] = chan->openalStreamingBuffer[0];
					chan->lastopenalStreamingBuffer[1] = chan->openalStreamingBuffer[1];
					chan->lastopenalStreamingBuffer[2] = chan->openalStreamingBuffer[2];
					alGenBuffers( 3, &chan->openalStreamingBuffer[0] );
					buffers[0] = chan->openalStreamingBuffer[0];
					buffers[1] = chan->openalStreamingBuffer[1];
					buffers[2] = chan->openalStreamingBuffer[2];
					finishedbuffers = 3;
				} else {
					alGetSourcei( chan->openalSource, AL_BUFFERS_PROCESSED, &finishedbuffers );
					alSourceUnqueueBuffers( chan->openalSource, finishedbuffers, &buffers[0] );
					if ( finishedbuffers == 3 ) {
						chan->triggered = true;
					}
				}

				for ( j = 0; j < finishedbuffers; j++ ) {
					chan->GatherChannelSamples( chan->openalStreamingOffset * sample->objectInfo.nChannels, MIXBUFFER_SAMPLES * sample->objectInfo.nChannels, alignedInputSamples );
					for ( int i = 0; i < ( MIXBUFFER_SAMPLES * sample->objectInfo.nChannels ); i++ ) {
						if ( alignedInputSamples[i] < -32768.0f )
							((short *)alignedInputSamples)[i] = -32768;
						else if ( alignedInputSamples[i] > 32767.0f )
							((short *)alignedInputSamples)[i] = 32767;
						else
							((short *)alignedInputSamples)[i] = idMath::FtoiFast( alignedInputSamples[i] );
					}
					alBufferData( buffers[j], chan->leadinSample->objectInfo.nChannels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, alignedInputSamples, MIXBUFFER_SAMPLES * sample->objectInfo.nChannels * sizeof( short ), 44100 );
					chan->openalStreamingOffset += MIXBUFFER_SAMPLES;
				}

				if ( finishedbuffers ) {
					alSourceQueueBuffers( chan->openalSource, finishedbuffers, &buffers[0] );
				}
			}

			// (re)start if needed..
			if ( chan->triggered ) {
				alSourcePlay( chan->openalSource );
				chan->triggered = false;
			}
		}
	}
#if 1 // DG: I /think/ this was only relevant for the old sound backends?
	// FIXME: completely remove else branch, but for testing leave it in under com_asyncSound 2
	//        (which also does the old 92-100ms updates)
	else if( com_asyncSound.GetInteger() == 2 ) {

		if ( slowmoActive && !chan->disallowSlow ) {
			idSlowChannel slow = sound->GetSlowChannel( chan );

			slow.AttachSoundChannel( chan );

				if ( sample->objectInfo.nChannels == 2 ) {
					// need to add a stereo path, but very few samples go through this
					memset( alignedInputSamples, 0, sizeof( alignedInputSamples[0] ) * MIXBUFFER_SAMPLES * 2 );
				} else {
					slow.GatherChannelSamples( offset, MIXBUFFER_SAMPLES, alignedInputSamples );
				}

			sound->SetSlowChannel( chan, slow );
		} else {
			sound->ResetSlowChannel( chan );

			// if we are getting a stereo sample adjust accordingly
			if ( sample->objectInfo.nChannels == 2 ) {
				// we should probably check to make sure any looping is also to a stereo sample...
				chan->GatherChannelSamples( offset*2, MIXBUFFER_SAMPLES*2, alignedInputSamples );
			} else {
				chan->GatherChannelSamples( offset, MIXBUFFER_SAMPLES, alignedInputSamples );
			}
		}

		//
		// work out the left / right ear values
		//
		float	ears[6];
		if ( global || omni ) {
			// same for all speakers
			for ( int i = 0 ; i < 6 ; i++ ) {
				ears[i] = idSoundSystemLocal::s_globalFraction.GetFloat() * volume;
			}
			ears[3] = idSoundSystemLocal::s_subFraction.GetFloat() * volume;		// subwoofer

		} else {
			CalcEars( numSpeakers, spatializedOriginInMeters, listenerPos, listenerAxis, ears, spatialize );

			for ( int i = 0 ; i < 6 ; i++ ) {
				ears[i] *= volume;
			}
		}

		// if the mask is 0, it really means do every channel
		if ( !mask ) {
			mask = 255;
		}
		// cleared mask bits set the mix volume to zero
		for ( int i = 0 ; i < 6 ; i++ ) {
			if ( !(mask & ( 1 << i ) ) ) {
				ears[i] = 0;
			}
		}

		// if sounds are generally normalized, using a mixing volume over 1.0 will
		// almost always cause clipping noise.  If samples aren't normalized, there
		// is a good call to allow overvolumes
		if ( idSoundSystemLocal::s_clipVolumes.GetBool() && !( parms->soundShaderFlags & SSF_UNCLAMPED )  ) {
			for ( int i = 0 ; i < 6 ; i++ ) {
				if ( ears[i] > 1.0f ) {
					ears[i] = 1.0f;
				}
			}
		}

		// if this is the very first mixing block, set the lastV
		// to the current volume
		if ( current44kHz == chan->trigger44kHzTime ) {
			for ( j = 0 ; j < 6 ; j++ ) {
				chan->lastV[j] = ears[j];
			}
		}

		if ( numSpeakers == 6 ) {
			if ( sample->objectInfo.nChannels == 1 ) {
				SIMDProcessor->MixSoundSixSpeakerMono( finalMixBuffer, alignedInputSamples, MIXBUFFER_SAMPLES, chan->lastV, ears );
			} else {
				SIMDProcessor->MixSoundSixSpeakerStereo( finalMixBuffer, alignedInputSamples, MIXBUFFER_SAMPLES, chan->lastV, ears );
			}
		} else {
			if ( sample->objectInfo.nChannels == 1 ) {
				SIMDProcessor->MixSoundTwoSpeakerMono( finalMixBuffer, alignedInputSamples, MIXBUFFER_SAMPLES, chan->lastV, ears );
			} else {
				SIMDProcessor->MixSoundTwoSpeakerStereo( finalMixBuffer, alignedInputSamples, MIXBUFFER_SAMPLES, chan->lastV, ears );
			}
		}

		for ( j = 0 ; j < 6 ; j++ ) {
			chan->lastV[j] = ears[j];
		}

	}
#endif // 1/0

	soundSystemLocal.soundStats.activeSounds++;

}

/*
===============
idSoundWorldLocal::FindAmplitude

  this is called from the main thread

  if listenerPosition is NULL, this is being used for shader parameters,
  like flashing lights and glows based on sound level.  Otherwise, it is being used for
  the screen-shake on a player.

  This doesn't do the portal-occlusion currently, because it would have to reset all the defs
  which would be problematic in multiplayer
===============
*/
float idSoundWorldLocal::FindAmplitude( idSoundEmitterLocal *sound, const int localTime, const idVec3 *listenerPosition,
									   const s_channelType channel, bool shakesOnly ) {
	int		i, j;
	soundShaderParms_t *parms;
	float	volume;
	int		activeChannelCount;
	static const int AMPLITUDE_SAMPLES = MIXBUFFER_SAMPLES/8;
	float	sourceBuffer[AMPLITUDE_SAMPLES];
	float	sumBuffer[AMPLITUDE_SAMPLES];
	// work out the distance from the listener to the emitter
	float	dlen;

	if ( !sound->playing ) {
		return 0;
	}

	if ( listenerPosition ) {
		// this doesn't do the portal spatialization
		idVec3 dist = sound->origin - *listenerPosition;
		dlen = dist.Length();
		dlen *= DOOM_TO_METERS;
	} else {
		dlen = 1;
	}

	activeChannelCount = 0;

	for ( i = 0; i < SOUND_MAX_CHANNELS ; i++ ) {
		idSoundChannel	*chan = &sound->channels[ i ];

		if ( !chan->triggerState ) {
			continue;
		}

		if ( channel != SCHANNEL_ANY && chan->triggerChannel != channel) {
			continue;
		}

		parms = &chan->parms;

		int	localTriggerTimes = chan->trigger44kHzTime;

		bool looping = ( parms->soundShaderFlags & SSF_LOOPING ) != 0;

		// check for screen shakes
		float shakes = parms->shakes;
		if ( shakesOnly && shakes <= 0.0f ) {
			continue;
		}

		//
		// calculate volume
		//
		if ( !listenerPosition ) {
			// just look at the raw wav data for light shader evaluation
			volume = 1.0;
		} else {
			volume = parms->volume;
			volume = soundSystemLocal.dB2Scale( volume );
			if ( shakesOnly ) {
				volume *= shakes;
			}

			if ( listenerPosition && !( parms->soundShaderFlags & SSF_GLOBAL )  ) {
				// check for overrides
				float maxd = parms->maxDistance;
				float mind = parms->minDistance;

				if ( dlen >= maxd ) {
					volume = 0.0f;
				} else if ( dlen > mind ) {
					float frac = idMath::ClampFloat( 0, 1, 1.0f - ((dlen - mind) / (maxd - mind)));
					if ( idSoundSystemLocal::s_quadraticFalloff.GetBool() ) {
						frac *= frac;
					}
					volume *= frac;
				}
			}
		}

		if ( volume <= 0 ) {
			continue;
		}

		//
		// fetch the sound from the cache
		// this doesn't handle stereo samples correctly...
		//
		if ( !listenerPosition && chan->parms.soundShaderFlags & SSF_NO_FLICKER ) {
			// the NO_FLICKER option is to allow a light to still play a sound, but
			// not have it effect the intensity
			for ( j = 0 ; j < (AMPLITUDE_SAMPLES); j++ ) {
				sourceBuffer[j] = j & 1 ? 32767.0f : -32767.0f;
			}
		} else {
			int offset = (localTime - localTriggerTimes);	// offset in samples
			int size = ( looping ? chan->soundShader->entries[0]->LengthIn44kHzSamples() : chan->leadinSample->LengthIn44kHzSamples() );
			short *amplitudeData = (short *)( looping ? chan->soundShader->entries[0]->amplitudeData : chan->leadinSample->amplitudeData );

			if ( amplitudeData ) {
				// when the amplitudeData is present use that fill a dummy sourceBuffer
				// this is to allow for amplitude based effect on hardware audio solutions
				if ( looping ) offset %= size;
				if ( offset < size ) {
					for ( j = 0 ; j < (AMPLITUDE_SAMPLES); j++ ) {
						sourceBuffer[j] = j & 1 ? amplitudeData[ ( offset / 512 ) * 2 ] : amplitudeData[ ( offset / 512 ) * 2 + 1 ];
					}
				}
			} else {
				// get actual sample data
				chan->GatherChannelSamples( offset, AMPLITUDE_SAMPLES, sourceBuffer );
			}
		}
		activeChannelCount++;
		if ( activeChannelCount == 1 ) {
			// store to the buffer
			for( j = 0; j < AMPLITUDE_SAMPLES; j++ ) {
				sumBuffer[ j ] = volume * sourceBuffer[ j ];
			}
		} else {
			// add to the buffer
			for( j = 0; j < AMPLITUDE_SAMPLES; j++ ) {
				sumBuffer[ j ] += volume * sourceBuffer[ j ];
			}
		}
	}

	if ( activeChannelCount == 0 ) {
		return 0.0;
	}

	float high = -32767.0f;
	float low = 32767.0f;

	// use a 20th of a second
	for( i = 0; i < (AMPLITUDE_SAMPLES); i++ ) {
		float fabval = sumBuffer[i];
		if ( high < fabval ) {
			high = fabval;
		}
		if ( low > fabval ) {
			low = fabval;
		}
	}

	float sout;
	sout = atan( (high - low) / 32767.0f) / DEG2RAD(45);

	return sout;
}

/*
=================
idSoundWorldLocal::FadeSoundClasses

fade all sounds in the world with a given shader soundClass
to is in Db (sigh), over is in seconds
=================
*/
void	idSoundWorldLocal::FadeSoundClasses( const int soundClass, const float to, const float over ) {
	if ( soundClass < 0 || soundClass >= SOUND_MAX_CLASSES ) {
		common->Error( "idSoundWorldLocal::FadeSoundClasses: bad soundClass %i", soundClass );
	}

	idSoundFade	*fade = &soundClassFade[ soundClass ];

	int	length44kHz = soundSystemLocal.MillisecondsToSamples( over * 1000 );

	// if it is already fading to this volume at this rate, don't change it
	if ( fade->fadeEndVolume == to &&
		fade->fadeEnd44kHz - fade->fadeStart44kHz == length44kHz ) {
		return;
	}

	int	start44kHz;

	if ( fpa[0] ) {
		// if we are recording an AVI demo, don't use hardware time
		start44kHz = lastAVI44kHz + MIXBUFFER_SAMPLES;
	} else {
		start44kHz = soundSystemLocal.GetCurrent44kHzTime() + MIXBUFFER_SAMPLES;
	}

	// fade it
	fade->fadeStartVolume = fade->FadeDbAt44kHz( start44kHz );
	fade->fadeStart44kHz = start44kHz;
	fade->fadeEnd44kHz = start44kHz + length44kHz;
	fade->fadeEndVolume = to;
}

/*
=================
idSoundWorldLocal::SetSlowmo
=================
*/
void idSoundWorldLocal::SetSlowmo( bool active ) {
	slowmoActive = active;
}

/*
=================
idSoundWorldLocal::SetSlowmoSpeed
=================
*/
void idSoundWorldLocal::SetSlowmoSpeed( float speed ) {
	slowmoSpeed = speed;
}

/*
=================
idSoundWorldLocal::SetEnviroSuit
=================
*/
void idSoundWorldLocal::SetEnviroSuit( bool active ) {
	enviroSuitActive = active;
}
