/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * Additional copyright for this file:
 * Copyright (C) 1999-2000 Revolution Software Ltd.
 * This code is based on source code created by Revolution Software,
 * used with permission.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "engines/icb/p4.h"
#include "engines/icb/common/px_common.h"
#include "engines/icb/common/px_sfx_description.h"
#include "engines/icb/common/px_linkeddatafile.h"
#include "engines/icb/common/px_clu_api.h"
#include "engines/icb/icb.h"
#include "engines/icb/debug.h"
#include "engines/icb/sound.h"
#include "engines/icb/sound_lowlevel.h"
#include "engines/icb/global_objects.h"
#include "engines/icb/res_man.h"
#include "engines/icb/session.h"
#include "engines/icb/mission.h"
#include "engines/icb/common/px_sound_constants.h"
#include "engines/icb/sound_logic.h"
#include "engines/icb/remora.h"

#include "common/textconsole.h"

namespace ICB {

const char *menuSoundID = "__MENU__";
uint32 menuSoundIDHash = NULL_HASH;

// max is 127, min is zero
int32 speechVolume = 64;
int32 sfxVolume = 64;
int32 musicVolume = 64;

int32 currentSoundLevel;

int32 GetCurrentSoundLevel() { return currentSoundLevel; }

int32 GetSpeechVolume() { return speechVolume; }

int32 GetSfxVolume() { return sfxVolume; }

int32 GetMusicVolume() { return musicVolume; }

void SetSpeechVolume(int32 v) {
	if ((v < 0) || (v > 128))
		Fatal_error("Speech volume must be 0-128 not %d", v);
	speechVolume = v;
}

void SetSfxVolume(int32 v) {
	if ((v < 0) || (v > 128))
		Fatal_error("Sfx volume must be 0-128 not %d", v);
	sfxVolume = v;
	// sfx volume will change next update...
}

void SetMusicVolume(int32 v) {
	if ((v < 0) || (v > 128))
		Fatal_error("Music volume must be 0-128 not %d", v);
	musicVolume = v;
}

// max volume
#define MAX_VOLUME 127
#define SPEECH_ON_VOLUME 48
#define REMORA_ACTIVE_VOLUME 24

#define RESERVED_CHANNELS ((1 << SPEECH_CHANNEL) | (1 << MUSIC_CHANNEL))

#define NUMBER_CHANNELS 24

#define MAX_Y_DISTANCE_SQR (200 * 200)

bool8 soundOn = TRUE8;

bool8 pauseSound = TRUE8;

int32 speechOnSliderValue = MAX_VOLUME;

const uint8 pitchMakerMults[12 * 16] = PITCH_MULT;
const uint8 pitchMakerDivs[12 * 16] = PITCH_DIV;

class PitchMaker {

#define STANDARD_PITCH 4096

public:
	int32 GetPitch(int32 initialPitch, int32 envValue) {
		int32 v;
		int32 e;

		e = envValue;
		v = STANDARD_PITCH;

		// shifting up an octave
		while (e >= (128 * 12)) {
			v <<= 1; // *2
			e -= (128 * 12);
		}

		// shifting down an octave
		while (e <= (-128 * 12)) {
			v >>= 1; // /2
			e += (128 * 12);
		}

		// divide by 8 so there are 16 per semi-tone
		e /= 8;

		// use exp tables for - and + envValue
		if (e < 0) {
			v = (v * pitchMakerDivs[-e]) / 128;
		} else if (e > 0) {
			v = (v * pitchMakerMults[e]) / 128;
		}

		return (initialPitch * v) / STANDARD_PITCH;
	}
};

PitchMaker pitchMaker;

const uint8 volMakerFunction[MAX_VOLUME + 1] = VOL_FUNCTION;

class VolMaker {

	  public:
	int32 GetVol(int32 i) {
		if (i < 0)
			return 0;
		else if (i > 127)
			return 127;
		else
			return (sfxVolume * volMakerFunction[i]) >> 7; // sfxVolume is 0 (none) to full (128)
	}

	int32 GetVolMusic(int32 i) {
		if (i < 0)
			return 0;
		else if (i > 127)
			return 127;
		else
			return (musicVolume * volMakerFunction[i]) >> 7; // sfxVolume is 0 (none) to full (128)
	}
};

VolMaker volMaker;

const char *gunSfxVar = "gun_sfx";
const char *defaultGunSfx = "gunshot";
const char *gunDesc = "gunshot";

const char *ricochetSfxVar = "ricochet_sfx";
const char *defaultRicochetSfx = "ricochet";
const char *ricochetDesc = "ricochet";

const char *openSfxVar = "open_sfx";
const char *defaultOpenSfx = "door_test";
const char *openDesc = "open";

const char *closeSfxVar = "close_sfx";
const char *defaultCloseSfx = "door_test";
const char *closeDesc = "close";

const char *addingMediSfxVar = "add_medi_sfx";
const char *defaultAddingMediSfx = "add_medi";
const char *defaultUsingMediSfx = "use_medi";
const char *addingMediDesc = "adding medipack";

const char *addingClipSfxVar = "add_clip_sfx";
const char *defaultAddingClipSfx = "add_clip";
const char *addingClipDesc = "adding clip";

const char *activateRemoraSfxVar = "remora_enter_sfx";
const char *activateRemoraSfx = "remora_enter";
const char *activateRemoraDesc = "entering Remora";

const char *emailSfxVar = "remora_enter_sfx";
const char *defaultEmailSfx = "remora_enter";
const char *emailDesc = "email arriving";

const char *deactivateRemoraSfxVar = "remora_exit_sfx";
const char *deactivateRemoraSfx = "remora_exit";
const char *deactivateRemoraDesc = "exiting Remora";

const char *tinkleSfxVar = "tinkle_sfx";
const char *defaultTinkleSfx = "cartridge_tinkle";
const char *tinkleDesc = "tinkle";

const char *menuUpDownSfx = "menu\\menu_up_down";
const char *menuSelectSfx = "menu\\menu_select";
const char *menuCancelSfx = "menu\\menu_cancel";

#define INT_1 128

// returns the value given by env at x (using INT maths)
// env is in format where 128=1
// x is in same units...
int32 EvalEnv(const CEnvelope &env, int32 x) {

	int32 pa = env.a;
	int32 pb = env.b;
	int32 pc = env.c;
	int32 pd = env.d;
	int32 xxx, xx;

	if (pa == 0)
		xxx = 0;
	else if (abs(pa) < INT_1)
		xxx = (pa * x * x * x) / (INT_1 * INT_1 * INT_1);
	else if (abs(pa) < (INT_1 * INT_1))
		xxx = (pa * (x * x * x / INT_1)) / (INT_1 * INT_1);
	// xxx=((pd/INT_1)*x*x*x)/(INT_1*INT_1);
	else if (abs(pa) < (INT_1 * INT_1 * INT_1))
		xxx = ((pa / INT_1) * (x * x * x / (INT_1))) / (INT_1);
	else
		xxx = ((pa / (INT_1 * INT_1)) * (x * x * x / INT_1));
	// xxx=((pd/(INT_1*INT_1))*x*x*x)/(INT_1);

	if (pb == 0)
		xx = 0;
	else if (abs(pb) < (INT_1 * INT_1))
		xx = (pb * x * x) / (INT_1 * INT_1);
	else if (abs(pb) < (INT_1 * INT_1 * INT_1))
		xx = (pb / INT_1) * (x * x / INT_1);
	else
		xx = (pb / (INT_1 * INT_1)) * (x * x);

	return xxx + xx + ((x * pc) / (INT_1)) + (pd);
}

// get the sfxlist file!
_linked_data_file *GetMissionSfxFile() {
	uint32 fileHash;
	uint32 clusterHash;
	_linked_data_file *f = nullptr;

	// if no mission return NULL
	if (!g_mission) {
		// PC NEEDS THIS!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		Fatal_error("No global mission sound so no special sfx!");

	}
	// get the file...
	else {

		fileHash = NULL_HASH;
		clusterHash = MS->Fetch_session_cluster_hash();
		f = (_linked_data_file *)private_session_resman->Res_open("m_sfxlist", fileHash, MS->Fetch_session_cluster(), clusterHash);

	}

	if ((f->GetHeaderVersion() != SFX_VERSION) || (f->header.type != FT_COMPILED_SFX))
		Fatal_error("Sound: mission::the.cmpsfxlist, Header wrong, engine:%d,%08x file:%d,%08x\n", SFX_VERSION, FT_COMPILED_SFX, f->GetHeaderVersion(), f->header.type);

	return f;
}

_linked_data_file *GetSessionSfxFile() {

	// if no session return NULL
	if ((!g_mission) || (!(g_mission->session))) {
		warning("no session so no sfx file!");
		return nullptr;
	}

	uint32 fileHash = NULL_HASH;
	uint32 clusterHash = MS->Fetch_session_cluster_hash();
	_linked_data_file *f;

	// For the PC clustering the sfx file does not have the path, just the name

	f = (_linked_data_file *)private_session_resman->Res_open(

	    "s_sfxlist",

	    fileHash, MS->Fetch_session_cluster(), clusterHash);

	if ((f->GetHeaderVersion() != SFX_VERSION) || (f->header.type != FT_COMPILED_SFX))
		Fatal_error("Sound: session::the.cmpsfxlist, Header wrong, engine:%d,%08x file:%d,%08x\n", SFX_VERSION, FT_COMPILED_SFX, f->GetHeaderVersion(), f->header.type);
	return f;
}

// get a pointer to sfx (number) in the mission or session sfx file
CSfx *GetMissionSfx(int32 number) {
	_linked_data_file *linkedSfx;

	linkedSfx = GetMissionSfxFile();

	return (CSfx *)linkedSfx->Fetch_item_by_number(number);
}

CSfx *GetSessionSfx(int32 number) {
	_linked_data_file *linkedSfx;

	linkedSfx = GetSessionSfxFile();

	return (CSfx *)linkedSfx->Fetch_item_by_number(number);
}

// return a number for the sfx (in either mision or session) -1 means the sfx is not in the sound file (probabily in the other one)
int32 WhichMissionSfx(uint32 sfx) {
	_linked_data_file *linkedSfx;
	uint32 n;

	linkedSfx = GetMissionSfxFile();
	if (linkedSfx == nullptr)
		return -1;

	n = linkedSfx->Fetch_item_number_by_hash(sfx);

	if (n == PX_LINKED_DATA_FILE_ERROR)
		return -1;
	else
		return (int32)n;
}

int32 WhichSessionSfx(uint32 sfx) {
	_linked_data_file *linkedSfx;
	uint32 n;

	linkedSfx = GetSessionSfxFile();
	if (linkedSfx == nullptr)
		return -1;

	n = linkedSfx->Fetch_item_number_by_hash(sfx);

	if (n == PX_LINKED_DATA_FILE_ERROR)
		return -1;
	else
		return (int32)n;
}

bool8 SfxExists(uint32 sfxHash) {
	int32 sfxNumber;

	sfxNumber = WhichMissionSfx(sfxHash);

	if (sfxNumber == -1) {
		sfxNumber = WhichSessionSfx(sfxHash);
	}

	if (sfxNumber == -1)
		return FALSE8;
	else
		return TRUE8;
}

bool8 SfxExists(const char *sfx) { return SfxExists(HashString(sfx)); }

int32 channelUsage = 0;

#define IS_CHANNEL_USED(CH) ((channelUsage | RESERVED_CHANNELS) & (1 << CH))
#define IS_CHANNEL_FREE(CH) ((~(channelUsage | RESERVED_CHANNELS)) & (1 << CH))
#define SET_CHANNEL_USED(CH)                                                                                                                                                       \
	{ channelUsage |= (1 << CH); }
#define SET_CHANNEL_FREE(CH)                                                                                                                                                       \
	{ channelUsage &= (~(1 << CH)); }

int32 GetFreeChannel() {
	int32 i;
	for (i = 0; i < NUMBER_CHANNELS; i++)
		if (IS_CHANNEL_FREE(i)) {
			return i;
		}
	return -1;
}

#define UPDATES_PER_SECOND 10
#define MAX_ENV_POSITION (128 * 128)

#define VOLUME_SLIDE 48 // how quickly we fade out a sound when it's time to cut off (this happens rarely but we don't want a pop)

// Get sfx for this registered sound
CSfx *CRegisteredSound::GetSfx() {
	CSfx *the_sfx = nullptr;

	if (m_sfxNumber == -1)
		Fatal_error("sfx is not found in session or mission");

	if (m_inSession)
		the_sfx = GetSessionSfx(m_sfxNumber);
	else
		the_sfx = GetMissionSfx(m_sfxNumber);

	if (!the_sfx)
		Fatal_error("Can't find registered SFX??? (number %d m_inSession=%d)", m_sfxNumber, m_inSession);

	return the_sfx;
}

// Zero out all the values of a sound
void CRegisteredSound::Wipe() {
	m_objID = NO_REGISTERED_SOUND;
	m_sndHash = NULL_HASH;
	m_channel = -1;

	m_x = REAL_ZERO;
	m_y = REAL_ZERO;
	m_z = REAL_ZERO;

	m_restart_time = -1;

	m_sfxNumber = NULL_HASH;

	m_velocity = 0;
	m_position = 0; // position*128

	m_volume = 0;
	m_current_pitch = 0;
	m_sample_pitch = 0;
	m_rand_pitch_value = 0;
	m_next_random_pos = 0;
	m_pan = 0;

	m_xoffset = REAL_ZERO;
	m_yoffset = REAL_ZERO;
	m_zoffset = REAL_ZERO;

	m_objMoving = 0;
	m_volume_offset = 0;
	m_inSession = FALSE8;
	m_turnOff = FALSE8;
	m_remove = FALSE8;
}

// update a sound if it is being turned off (reduce volume until 0 then destroy)
void CRegisteredSound::TurnOff() {
	if (m_turnOff) {
		if (m_channel == -1) {
			m_turnOff = FALSE8;
			if (m_remove)
				m_objID = NO_REGISTERED_SOUND; // remove sound
			return;
		}

		if (m_volume == 0) {
			Tdebug("sounds.txt", "Finally turning off %d!", m_channel);

			if (soundOn)
				StopSample(m_channel);

			SET_CHANNEL_FREE(m_channel);
			m_channel = -1;
			m_turnOff = FALSE8;
			if (m_remove)
				m_objID = NO_REGISTERED_SOUND; // remove sound
		} else {
			m_volume -= VOLUME_SLIDE;
			if (m_volume < 0)
				m_volume = 0;

			if (soundOn)
				SetChannelVolumeAndPan(m_channel, volMaker.GetVol(m_volume), m_pan);
		}
	}
}

// update every game cycle...
void CRegisteredSound::UpdateGameCycle(int32 newVol, int32 newPan) {
	CSfx *sfx;

	if (m_objID == NO_REGISTERED_SOUND)
		return;

	// if we're beyond next random update point (this must be done before wrap around...
	sfx = GetSfx();

	// new random pitch value
	if ((sfx->m_rand_mode) && (m_position > m_next_random_pos)) {
		GetRandom(sfx);
		m_next_random_pos += (128 * 128 - 1) / (sfx->m_rand_mode);
	}

	// stop sound if nesesary or loop
	if (m_position > MAX_ENV_POSITION) {
		if (!((sfx->m_looping) & SFX_LOOPING_FLAG)) {
			m_position = MAX_ENV_POSITION - 1; // hold here while turning off...

			Tdebug("sounds.txt", "sound ending");

			// turn off next cycle..
			if (m_channel != -1) {
				m_turnOff = TRUE8; // this will keep updating until m_channel is set to -1 by turn off at which point the sound will end
				m_remove = TRUE8;  // remove when finished turning off
			} else
				m_objID = NO_REGISTERED_SOUND; // don't need to update anymore...
		} else {
			// Tdebug("sounds.txt","sound looping...");

			m_next_random_pos = 0; // reset random pos thingy

			// if sample is not looping then replay...
			if ((m_channel != -1) && (!((sfx->m_looping) & WAV_LOOPING_FLAG))) {
				m_position = 0;
			} else {                                // sample is looping or sound isn't on so just reset wave
				m_position -= MAX_ENV_POSITION; // reset wave

				if (m_position <= 0) // definately don't restart accidently
					m_position = 1;
			}
		}
	}

	if (m_position < 0)
		m_position++;

	// if we're currently playing then update the low level if m_position=0 then time to start up sound
	if ((m_channel != -1) && (m_position >= 0)) {
		int32 v, p;
		int32 myPos;

		myPos = (m_position * (sfx->m_pitch.div)) & (128 * 128 - 1);                                        // work out where we are on the pitch graph...
		p = pitchMaker.GetPitch(m_sample_pitch, m_rand_pitch_value + EvalEnv(sfx->m_pitch, myPos / INT_1)); // get envelope pitch

		// if paused sounds then set pitch to zero

		if ((pauseSound) && ((m_sndHash != menuSoundIDHash) || (m_objID != SPECIAL_SOUND))) {
			// printf("channel %d pitch is now 0\n",m_channel);
			p = 0;
		}

		// has changed so update pitch

		if (p != m_current_pitch) {
			m_current_pitch = p;

			if (soundOn)
				SetChannelPitch(m_channel, m_current_pitch);
		}

		// VOLUME

		if (m_turnOff) {
			v = m_volume; // if we're turning off then ignore volume envelope
		} else {
			myPos = (m_position * (sfx->m_volume.div)) & (128 * 128 - 1); // volume pos
			v = (newVol * EvalEnv(sfx->m_volume, myPos / INT_1)) / 128;   // get new volume from envelope and input volume

			// decrease volume if speech is playing...
			v = (v * speechOnSliderValue) / 128;

			// limit to 0-127 for volume
			if (v > MAX_VOLUME)
				v = MAX_VOLUME;
			else if (v < 0)
				v = 0;
		}

		// current sound level is measure of how much sound is in scene
		// here it is both affected by distance from player and
		// volume on the curve...
		currentSoundLevel += (v * v);

		// if either volume or pan has changed

		if ((v != m_volume) || (newPan != m_pan)) {
			m_volume = v;
			m_pan = newPan;

			// only set if sound on
			if (soundOn)
				SetChannelVolumeAndPan(m_channel, volMaker.GetVol((m_volume * m_volume_offset) / 128), m_pan);
		}

		// if we need to start the sound effect

		if (m_position == 0) {
			Tdebug("sounds.txt", "Starting sound");

			m_position = 1; // so we don't try and start it twice in the unlikely event
			// that no 10hz signal goes between calls...

			// only start sample if sound is on
			if (soundOn) {
				StartSample(m_channel, sfx->GetSampleName(), m_inSession, (sfx->m_looping) & WAV_LOOPING_FLAG);
			}
		}
	}
}

// update at 10hz EXACTLY
void CRegisteredSound::Update10Hz() {
	if (m_objID == NO_REGISTERED_SOUND)
		return;

	// only update special menu sounds during 10hz signal...
	// if sounds are paused and sound is not a special menu sound (either sndHash doesn't match or sound object is not special)
	if ((pauseSound) && ((m_sndHash != menuSoundIDHash) || (m_objID != SPECIAL_SOUND)))
		return;

	if (m_position >= 0)
		m_position += m_velocity;
}

int32 GetSoundCloser(int32 objID, PXreal x, PXreal y, PXreal z);

// make a sound hearable, get a channel and set to it...
bool8 CRegisteredSound::SetHearable() {
	int32 ch;

	// sound is already hearable so ignore this call
	if (m_channel != -1)
		return TRUE8;

	Tdebug("sounds.txt", "Sound is now hearable");

	// find a free channel
	ch = GetFreeChannel();
	Tdebug("sounds.txt", "Channel %d", ch);

	// if we couldn't get a channel then we might want to knock out another sound
	if (ch == -1) {

		ch = GetSoundCloser(m_objID, m_x, m_y, m_z);
		if (ch == -1)
			return TRUE8; // still don't return true just dont set a channel
	}

	SET_CHANNEL_USED(ch);

	// reset volume and pitch ready to be set (keep existing pan)
	m_volume = 0xffff;
	m_current_pitch = 0xffff;

	// set channel
	m_channel = ch;

	m_position = m_restart_time; // start sample at next update...
	m_restart_time = -1;

	return TRUE8;
}

// make a sound unhearable, remove from playing sounds list...
void CRegisteredSound::SetUnhearable() {
	// already unhearable or turning off so do nothing...
	if ((m_channel == -1) || (m_turnOff))
		return;

	Tdebug("sounds.txt", "Sound is now unhearable");

	// start turning off sound... (we are definately playing it...)
	m_turnOff = TRUE8;
	// don't remove though
}

// remove the sound
void CRegisteredSound::Remove() {
	// stop sample etc
	if (m_channel == -1) {
		m_objID = NO_REGISTERED_SOUND; // just remove it
	} else {
		m_turnOff = TRUE8; // fade down
		m_remove = TRUE8;  // then remove
	}
}

void CRegisteredSound::GetRandom(CSfx *sfx) {
	if (sfx->m_rand_pitch)
		m_rand_pitch_value = (g_icb->getRandomSource()->getRandomNumber(2 * 128 * sfx->m_rand_pitch - 1)) - (128 * sfx->m_rand_pitch);
	else
		m_rand_pitch_value = 0;
}

// register a new sound setting internal params
void CRegisteredSound::Register(const char *sndName, const char *sfxName, uint32 sfxHash, int8 volume) {
	CSfx *sfx;

	// simple params
	m_sndHash = HashString(sndName);

	if (sfxHash == NULL_HASH)
		sfxHash = HashString(sfxName);

	m_sfxNumber = WhichMissionSfx(sfxHash);
	m_inSession = FALSE8;

	if (m_sfxNumber == -1) {
		m_sfxNumber = WhichSessionSfx(sfxHash);
		m_inSession = TRUE8;
	}

	if (m_sfxNumber == -1)
		Fatal_error("sfx %s(%08x) is not found in session or mission", sfxName, sfxHash);

	// check it's okay...
	sfx = GetSfx();

	// set velocity = MAX_POSITION/((duration/128)*UPDATES_PER_SECOND)= 128*128*128/(duration*UPDATES_PER_SECOND)
	m_velocity = (MAX_ENV_POSITION * 128) / (sfx->m_duration * UPDATES_PER_SECOND);
	Tdebug("sounds.txt", "length=%d secs vel %d\n", (sfx->m_duration) / 128, m_velocity);

	// start next frame..
	m_restart_time = -1; // when hearable start at -1
	m_position = -1;

	m_channel = -1;

	m_volume = 0; // default, gets changed if sound is heard
	m_pan = 0;    // default (centre) will get changed before playing if necesary

	m_sample_pitch = GetSamplePitch(sfx->GetSampleName(), m_inSession);

	m_current_pitch = 0; // always alter pitch before playing

	GetRandom(sfx);
	m_next_random_pos = 0;

	m_remove = FALSE8;
	m_turnOff = FALSE8;

	m_xoffset = m_yoffset = m_zoffset = (PXreal)0;

	m_volume_offset = volume;

	padding1 = 0;
}

// register a sound and take position from the position of the object registering the sound
void CRegisteredSound::RegisterFromObject(const uint32 objID, const char *sndName, const char *sfxName, uint32 sfxHash, PXreal xo, PXreal yo, PXreal zo, int8 volume) {
	Register(sndName, sfxName, sfxHash, volume);
	m_xoffset = xo;
	m_yoffset = yo;
	m_zoffset = zo;

	// set this last so any updates don't know it's turned on yet...
	m_objID = objID;

	if ((MS->logic_structs[m_objID]->image_type) == VOXEL) {
		Tdebug("sounds.txt", "sound creator is an actor");
		m_objMoving = 1;
	} else
		m_objMoving = 0;

	MS->logic_structs[m_objID]->GetPosition(m_x, m_y, m_z);

	// This tells the sound logic about the new sound, to drive events etc.
	g_oSoundLogicEngine->NewSound(objID, (int32)m_x, (int32)m_y, (int32)m_z, GetSfx(), m_sndHash);
}

// register a sound as an absolute position
void CRegisteredSound::RegisterFromAbsolute(const uint32 objID, const char *sndName, const char *sfxName, uint32 sfxHash, PXreal x, PXreal y, PXreal z, int8 volume) {

	Register(sndName, sfxName, sfxHash, volume);

	// set this last so any updates don't know it's turned on yet...
	m_objID = objID;

	m_x = x;
	m_y = y;
	m_z = z;
	m_objMoving = 0;
}

#define MAX_SCREEN (float)(SCREEN_WIDTH / 2)

// if the sound if coming froma moving souce, this updates the position, otherwise it does nothing
void CRegisteredSound::GetPosition() {
	if (m_objMoving) {
		MS->logic_structs[m_objID]->GetPosition(m_x, m_y, m_z);
		m_x += m_xoffset;
		m_y += m_yoffset;
		m_z += m_zoffset;
	}
}

// gets the volume and pan values for a sound, based on the position of the sound relative to the actor / camera
void CRegisteredSound::GetVolumeAndPan(int32 &vol, int32 &pan) {
	// special sound, always central, etc
	if (m_objID == SPECIAL_SOUND) {
		vol = (int32)m_z; // get sound from z position...!
		pan = (int32)m_x; // pan is x position!
		return;
	}

	GetPosition();
	vol = g_oSoundLogicEngine->ProcessSound((int32)m_x, (int32)m_y, (int32)m_z, GetSfx());

	PXvector v = {m_x, m_y, m_z};
	PXvector screenPos;
	bool8 dontUse;

	if (!MS->SetOK()) {
		pan = 0;
		return;
	}

	PXcamera &cam = MS->GetCamera();
	PXWorldToFilm(v, cam, dontUse, screenPos);

	if (screenPos.x < (-MAX_SCREEN))
		screenPos.x = (-MAX_SCREEN);
	if (screenPos.x > MAX_SCREEN)
		screenPos.x = MAX_SCREEN;

	// /2 for centralisation
	pan = (int32)(128 * screenPos.x) / (SCREEN_WIDTH);

	return;
}

CRegisteredSound *g_registeredSounds[MAX_REGISTERED_SOUNDS];

int32 assignedSounds = 0;

// must be called at 10hz time...
void UpdateSounds10Hz() {
	int32 i;

	for (i = 0; i < MAX_REGISTERED_SOUNDS; i++) {
		if (g_registeredSounds[i])
			g_registeredSounds[i]->Update10Hz();
	}
}

// called every game cycle sets hearable and unhearable sounds...
void UpdateHearableSounds() {
	int32 i;
	int32 assigned;
	int32 volume;
	int32 pan;

	assigned = 0;

	// no sound...
	currentSoundLevel = 0;

	for (i = 0; i < MAX_REGISTERED_SOUNDS; i++) {
		// we might need to do this even if sound is not currently used
		g_registeredSounds[i]->TurnOff();

		if (g_registeredSounds[i]->IsUsed()) {
			// work out if is current hearable by the camera

			// if speical sound or sound is not paused then we get the volume and pan from manager
			if (((g_registeredSounds[i]->m_objID == SPECIAL_SOUND) && (g_registeredSounds[i]->m_sndHash == menuSoundIDHash)) || (!pauseSound)) {
				g_registeredSounds[i]->GetVolumeAndPan(volume, pan);

				// if volume is greater than zero
				if (volume > 0) {
					if (!g_registeredSounds[i]->SetHearable())
						Fatal_error("Can't find a free channel to play sound");
				} else
					g_registeredSounds[i]->SetUnhearable(); // no Update required we are turning off or are off screen....
			} else {
				volume = 0;
				pan = 0;
			}

			// update the wave etc
			g_registeredSounds[i]->UpdateGameCycle(volume, pan);
		}
	}

	currentSoundLevel = (currentSoundLevel * 100) / (128 * 128);
	if (currentSoundLevel > 100)
		currentSoundLevel = 100;

	assignedSounds = assigned;

	// speech volume slider

	int32 speechOnSliderTarget;

	// if somebody speaking...
	if ((g_mission) && (g_mission->session) && (MS->speech_info[CONV_ID].total_subscribers > 0) && (GetSpeechVolume() > 0))
		speechOnSliderTarget = SPEECH_ON_VOLUME;
	// if remora active even lower volume
	else if (g_oRemora->IsActive())
		speechOnSliderTarget = REMORA_ACTIVE_VOLUME;
	// otherwise our target is full volume
	else
		speechOnSliderTarget = MAX_VOLUME;

	// update
	if (speechOnSliderValue > speechOnSliderTarget) {
		speechOnSliderValue -= VOLUME_SLIDE;
		if (speechOnSliderValue < speechOnSliderTarget)
			speechOnSliderValue = speechOnSliderTarget;
	} else if (speechOnSliderValue < speechOnSliderTarget) {
		speechOnSliderValue += VOLUME_SLIDE;
		if (speechOnSliderValue > speechOnSliderTarget)
			speechOnSliderValue = speechOnSliderTarget;
	}
}

int32 GetSoundCloser(int32 objID, PXreal x, PXreal y, PXreal z) {
	PXreal px = REAL_ZERO;
	PXreal py = REAL_ZERO;
	PXreal pz = REAL_ZERO; // play pos

	PXreal dist;     // distance of this object
	PXreal thisDist; // distance of comparing object

	int32 i;
	int32 c;

	// if
	if (objID != SPECIAL_SOUND) {
		MS->player.log->GetPosition(px, py, pz);

		x -= px;
		y -= py;
		z -= pz;
		dist = x * x + y * y + z * z;
	} else // special sound so distance is by definition 0
		dist = (PXreal)0;

	PXreal maxDist = dist;
	int32 sfxReplace = -1;
	int32 channelReplace = -1;

	for (i = 0; i < MAX_REGISTERED_SOUNDS; i++) {
		c = g_registeredSounds[i]->m_channel;

		// can only replace a sound that has a channel and is not a special sound
		if ((c != -1) && (g_registeredSounds[i]->m_objID != SPECIAL_SOUND)) {
			g_registeredSounds[i]->GetPosition();
			x = g_registeredSounds[i]->m_x - px;
			y = g_registeredSounds[i]->m_y - py;
			z = g_registeredSounds[i]->m_z - pz;
			thisDist = x * x + y * y + z * z;

			// if this distance from player is more than the maximum so far then this is the furthest sound
			// yet found, and should therefore be replaced...
			if (thisDist > maxDist) {
				// this is the new max distance
				maxDist = thisDist;

				// this is the sfx and channcel to take
				sfxReplace = i;
				channelReplace = c;
			}
		}
	}

	// we found a sound further away than us therefore we can have it's channcel
	// the sfx is sfxReplace, the channel we are stealing is channelReplace
	if (channelReplace != -1) {
		Tdebug("sounds.txt", "replacing sound %d (channel %d) because it's too far away (dist: %g, our dist: %g)", sfxReplace, channelReplace, maxDist, dist);

		// the sfx now has no channel
		g_registeredSounds[sfxReplace]->m_channel = -1;

		// we return the channel we have just pinched

		return channelReplace;
	}

	// otherwise we found no channel so return -1
	return -1;
}

void TurnOffSound() { soundOn = FALSE8; }

void TurnOnSound() { soundOn = TRUE8; }

int32 GetFreeSound(const char *sfxName) {
	int32 i;

	for (i = 0; i < MAX_REGISTERED_SOUNDS; i++) {
		if (g_registeredSounds[i]->IsFree()) {
			Tdebug("sounds.txt", "sfx: %s  registered sound: %d", sfxName, i);
			return i;
		}
	}

	Fatal_error("No free sounds! %s", sfxName);
	return -1;
}

int32 FindSound(uint32 obj, uint32 sndHash, int32 start = 0) {
	int32 i;

	for (i = start; i < MAX_REGISTERED_SOUNDS; i++) {
		if (g_registeredSounds[i]->IsThisSound(obj, sndHash))
			return i;
	}

	// should this be a fatal error..??
	return -1;
}

// fn_play_sfx_offset(sfxName,sndID,x,y,z,isNico)
// this is the generic function
void RegisterSoundOffset(uint32 obj, const char *offsetName, const char *sfxName, uint32 sfxHash, const char *sndID, PXreal xo, PXreal yo, PXreal zo, int32 isNico, int32 time,
						 int8 volume_offset) {
	// check we have the hash before we register any sounds...!
	if (menuSoundIDHash == NULL_HASH)
		menuSoundIDHash = HashString(menuSoundID);

	int32 i;
	i = GetFreeSound(sfxName);

	// for getting nico position
	PXreal x, y, z;

	if ((obj != SPECIAL_SOUND) && (pauseSound))
		warning("Registering sound whilst sound paused!");

	// special sound
	if (obj == SPECIAL_SOUND) {
		g_registeredSounds[i]->RegisterFromAbsolute(obj, sndID, sfxName, sfxHash, xo, yo, zo, volume_offset);
	}
	// absolute sound (no name)
	else if ((offsetName == nullptr) || (strcmp(offsetName, "") == 0)) {
		// absolute address
		g_registeredSounds[i]->RegisterFromAbsolute(obj, sndID, sfxName, sfxHash, xo, yo, zo, volume_offset);
	}
	// object is a nico marker
	else if (isNico) {
		// is nico so get position of it...
		// x=, y=, z=
		_feature_info *fi = (_feature_info *)(MS->features->Fetch_item_by_name(offsetName));
		x = fi->x;
		y = fi->y;
		z = fi->z;
		g_registeredSounds[i]->RegisterFromAbsolute(obj, sndID, sfxName, sfxHash, x + xo, y + yo, z + zo, volume_offset);
	}
	// object is a mega
	else {
		// is mega object so attach sound to it
		// obj=
		obj = MS->objects->Fetch_item_number_by_name(offsetName);
		g_registeredSounds[i]->RegisterFromObject(obj, sndID, sfxName, sfxHash, xo, yo, zo, volume_offset);
	}

	Tdebug("sounds.txt", "restart time is %d %d\n", (-1) - (time), time);
	g_registeredSounds[i]->m_restart_time = (-1) - (time);
}

// register a sound from an object
void RegisterSound(uint32 obj, const char *sfxName, uint32 sfxHash, const char *sndID, int8 volume_offset) {
	const char *name;

	if (obj == SPECIAL_SOUND)
		name = nullptr;
	else
		name = (const char *)(MS->objects->Fetch_items_name_by_number(obj));

	RegisterSoundOffset(obj, name, sfxName, sfxHash, sndID, (PXreal)0, (PXreal)0, (PXreal)0, 0, 0, volume_offset);
}

// register a sound from an absolute position
void RegisterSoundAbsolute(uint32 obj, const char *sfxName, uint32 sfxHash, const char *sndID, PXreal x, PXreal y, PXreal z, int8 volume_offset) {
	RegisterSoundOffset(obj, nullptr, sfxName, sfxHash, sndID, x, y, z, 0, 0, volume_offset);
}

void RegisterSoundTime(uint32 obj, const char *sfxName, uint32 sfxHash, const char *sndID, int32 time, int8 volume_offset) {
	const char *name;

	if (obj == SPECIAL_SOUND)
		name = nullptr;
	else
		name = (const char *)(MS->objects->Fetch_items_name_by_number(obj));

	RegisterSoundOffset(obj, name, sfxName, sfxHash, sndID, (PXreal)0, (PXreal)0, (PXreal)0, 0, time, volume_offset);
}

// special sound
// for menus
void RegisterMenuSound(const char *sfxName, uint32 sfxHash, int32 volume, int32 pan, int8 volume_offset) {
	// volume is z of position
	RegisterSoundOffset(SPECIAL_SOUND, nullptr, sfxName, sfxHash, menuSoundID, (PXreal)pan, (PXreal)0, (PXreal)volume, 0, 0, volume_offset);
}

// special sound
// for in game (these are paused just like any other...
void RegisterSoundSpecial(const char *sfxName, uint32 sfxHash, const char *sndID, int32 volume, int32 pan, int8 volume_offset) {
	// volume is z of position
	RegisterSoundOffset(SPECIAL_SOUND, nullptr, sfxName, sfxHash, sndID, (PXreal)pan, (PXreal)0, (PXreal)volume, 0, 0, volume_offset);
}

void RemoveRegisteredSound(uint32 obj, const char *sndID) {
	int32 i;
	uint32 sndIDHash;

	sndIDHash = HashString(sndID);

	int32 start;

	// do object sounds with id

	// start at beginning
	start = 0;

	do {
		i = FindSound(obj, sndIDHash, start);
		// okay we have one (either object or special)
		if (i != -1)
			g_registeredSounds[i]->Remove();

		// next start of search is one above this...
		start = i + 1;

	} while (i != -1);

	// now do special sounds with ID

	// restart
	start = 0;

	do {
		i = FindSound(SPECIAL_SOUND, sndIDHash, start);

		// okay we have one (either object or special)
		if (i != -1)
			g_registeredSounds[i]->Remove();

		// next start of search is one above this...
		start = i + 1;

	} while (i != -1);

	// okay all done
}

void RemoveAllSoundsWithID(uint32 obj) {
	int32 i;

	for (i = 0; i < MAX_REGISTERED_SOUNDS; i++) {
		if (g_registeredSounds[i]->GetObjectID() == obj)
			g_registeredSounds[i]->Remove();
	}
}

void StopAllSoundsNow() {
	Tdebug("sounds.txt", "stopping");
	uint32 i;

	for (i = 0; i < MAX_REGISTERED_SOUNDS; i++) {
		g_registeredSounds[i]->Wipe();
	}

	for (i = 0; i < NUMBER_CHANNELS; i++) {
		if (soundOn) {
			StopSample(i);
		}

		SET_CHANNEL_FREE(i);
	}

	Tdebug("sounds.txt", "stopped");
}

// pause all sounds except special sounds...
void PauseSounds() {
	pauseSound = TRUE8;
	UpdateHearableSounds();
}

void UnpauseSounds() {
	pauseSound = FALSE8;
}

} // End of namespace ICB
