You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
456 lines
13 KiB
456 lines
13 KiB
#include "quakedef.h"
|
|
|
|
#ifdef USE_FMOD
|
|
#include "fmod.h"
|
|
#include "fmod_errors.h"
|
|
|
|
extern qboolean sound_started; // in snd_dma.c
|
|
|
|
FMOD_SYSTEM *fmod_system = NULL;
|
|
static qboolean fmod_ownership = false;
|
|
static int fmod_samplerate;
|
|
static float old_volume = -1.0f;
|
|
|
|
static FMOD_CHANNELGROUP *sfx_channelGroup = NULL;
|
|
|
|
static const char *FMOD_SpeakerModeString(FMOD_SPEAKERMODE speakermode);
|
|
static float SND_FMOD_Attenuation(FMOD_CHANNELCONTROL *channelControl, float distance);
|
|
|
|
// Copy and convert coordinate system
|
|
#define FMOD_VectorCopy(a, b) {(b).x=(a)[0];(b).y=(a)[2];(b).z=(a)[1];}
|
|
#define sound_nominal_clip_dist 1000.0
|
|
|
|
typedef struct entchannel_s
|
|
{
|
|
int entnum;
|
|
int entchannel;
|
|
float dist_mult;
|
|
FMOD_CHANNEL *channel;
|
|
} entchannel_t;
|
|
|
|
static entchannel_t entchannels[MAX_CHANNELS];
|
|
|
|
void S_Startup(void)
|
|
{
|
|
FMOD_RESULT result;
|
|
FMOD_SPEAKERMODE speakermode;
|
|
unsigned int version;
|
|
int driver, numchannels;
|
|
char name[1024];
|
|
|
|
// Create FMOD System if it doesn't exist already
|
|
if (!fmod_system)
|
|
{
|
|
result = FMOD_System_Create(&fmod_system);
|
|
if (result != FMOD_OK)
|
|
{
|
|
Con_Printf("Failed to create FMOD System: %s\n", FMOD_ErrorString(result));
|
|
return;
|
|
}
|
|
|
|
fmod_ownership = true;
|
|
}
|
|
|
|
result = FMOD_System_GetVersion(fmod_system, &version);
|
|
if (result != FMOD_OK)
|
|
{
|
|
Con_Printf("Failed to retrieve FMOD version: %s\n", FMOD_ErrorString(result));
|
|
return;
|
|
}
|
|
|
|
result = FMOD_System_GetDriver(fmod_system, &driver);
|
|
if (result != FMOD_OK)
|
|
{
|
|
Con_Printf("Failed to retrieve selected FMOD driver: %s\n", FMOD_ErrorString(result));
|
|
return;
|
|
}
|
|
|
|
result = FMOD_System_GetDriverInfo(fmod_system, driver, name, sizeof(name), NULL, &fmod_samplerate, &speakermode, &numchannels);
|
|
if (result != FMOD_OK)
|
|
{
|
|
Con_Printf("Failed to retrieve FMOD driver info: %s\n", FMOD_ErrorString(result));
|
|
return;
|
|
}
|
|
|
|
Con_Printf("FMOD version %01x.%02x.%02x, driver '%s', %s speaker mode, %d Hz, %d channels\n",
|
|
(version >> 16) & 0xff, (version >> 8) & 0xff, version & 0xff, name, FMOD_SpeakerModeString(speakermode), fmod_samplerate, numchannels);
|
|
|
|
result = FMOD_System_CreateChannelGroup(fmod_system, "SFX", &sfx_channelGroup);
|
|
if (result != FMOD_OK)
|
|
{
|
|
Con_Printf("Failed to create FMOD SFX channel group: %s\n", FMOD_ErrorString(result));
|
|
return;
|
|
}
|
|
|
|
// Could use System::set3DRolloffCallback to set up a (attenuation / sound_nominal_clip_dist) distance multiplier (would need to use ChannelControl::setUserData to hold ref to attn value)
|
|
// Note: sound_nominal_clip_dist could be dynamic to allow a small sound 'bubble' for local multiplayer
|
|
FMOD_ChannelGroup_Set3DMinMaxDistance(sfx_channelGroup, 0.0f, sound_nominal_clip_dist);
|
|
FMOD_System_Set3DRolloffCallback(fmod_system, &SND_FMOD_Attenuation);
|
|
|
|
memset(entchannels, 0, sizeof(entchannels));
|
|
|
|
sound_started = true;
|
|
}
|
|
|
|
void S_Shutdown(void)
|
|
{
|
|
Con_DPrintf("[FMOD] Shutdown\n");
|
|
|
|
// Destroy any SFX channels still in use
|
|
|
|
// If we created the FMOD System (and consequently own it), destroy it here
|
|
if (fmod_ownership)
|
|
{
|
|
FMOD_System_Close(fmod_system);
|
|
fmod_system = NULL;
|
|
fmod_ownership = false;
|
|
}
|
|
}
|
|
|
|
static unsigned int SND_GetDelay(const sfx_t *sfx)
|
|
{
|
|
unsigned int delay;
|
|
|
|
delay = 0.1 * fmod_samplerate;
|
|
if (delay > sfx->samples)
|
|
delay = sfx->samples;
|
|
if (delay > 0)
|
|
delay = rand() % delay;
|
|
|
|
return delay;
|
|
}
|
|
|
|
static float SND_FMOD_Attenuation(FMOD_CHANNELCONTROL *channelcontrol, float distance)
|
|
{
|
|
FMOD_RESULT result;
|
|
entchannel_t *userdata;
|
|
float scale;
|
|
|
|
result = FMOD_Channel_GetUserData((FMOD_CHANNEL*)channelcontrol, &userdata);
|
|
if (result != FMOD_OK || !userdata)
|
|
{
|
|
// Unknown type of channel or maybe it's a channel group, either way just return full volume in this case
|
|
return 1.0f;
|
|
}
|
|
|
|
scale = 1.0f - (distance * userdata->dist_mult);
|
|
if (scale < 0.0f)
|
|
scale = 0.0f;
|
|
|
|
return scale;
|
|
}
|
|
|
|
//static entchannel_t *SND_PickChannel(int entnum, int entchannel)
|
|
//{
|
|
// int ch_idx;
|
|
// entchannel_t *channel;
|
|
// FMOD_BOOL isplaying;
|
|
//
|
|
// // Check for replacement sound, or find the best one to replace
|
|
// for (ch_idx = MAX_AMBIENTS; ch_idx < MAX_CHANNELS; ch_idx++)
|
|
// {
|
|
// channel = &entchannels[ch_idx];
|
|
//
|
|
// if (entchannel != 0 // channel 0 never overrides
|
|
// && channel->entnum == entnum
|
|
// && (channel->entchannel == entchannel || entchannel == -1))
|
|
// {
|
|
// // always override sound from same entity
|
|
// break;
|
|
// }
|
|
//
|
|
// // don't let monster sounds override player sounds
|
|
// if (channel->entnum == cl.viewentity && entnum != cl.viewentity && channel->sfx)
|
|
// continue;
|
|
//
|
|
// if (snd_channels[ch_idx].end - paintedtime < life_left)
|
|
// {
|
|
// life_left = snd_channels[ch_idx].end - paintedtime;
|
|
// first_to_die = ch_idx;
|
|
// }
|
|
// }
|
|
//
|
|
// if (!channel)
|
|
// return NULL;
|
|
//
|
|
// if (channel->sfx && channel->channel)
|
|
// {
|
|
// FMOD_Channel_IsPlaying(channel->channel, &isplaying);
|
|
// if (isplaying)
|
|
// {
|
|
// FMOD_Channel_Stop(channel->channel);
|
|
// }
|
|
// channel->channel = NULL;
|
|
// channel->sfx = NULL;
|
|
// }
|
|
//
|
|
// channel->entnum = entnum;
|
|
// channel->entchannel = entchannel;
|
|
// return channel;
|
|
//}
|
|
|
|
void S_StartSound(int entnum, int entchannel, sfx_t *sfx, vec3_t origin, float fvol, float attenuation) // Note: volume and attenuation are properly normalized here
|
|
{
|
|
FMOD_CHANNEL *channel;
|
|
FMOD_VECTOR position;
|
|
FMOD_RESULT result;
|
|
|
|
if (!fmod_system || !sfx)
|
|
return;
|
|
|
|
// TODO: check nosound cvar
|
|
//return; // HACK play no entity sfx for now
|
|
|
|
S_LoadSound(sfx);
|
|
if (!sfx->sound)
|
|
return;
|
|
|
|
// Find channel group for entity number (or create) => note entnum can also be some random hash value
|
|
// ChannelControl::setMode => set to 3D
|
|
// Can use channel group per entity to store entity-specific userdata, with maybe 16 fixed channel slots
|
|
|
|
// Pre-define channels for each entity (could be an array of ints, probably array of entchannel_t structs)
|
|
// For entchannel 0, dynamically select a free channel (or just play without doing anything, let FMOD handle it)
|
|
// If channel at index entchannel >0 is already playing: stop
|
|
// Set userdata pointer to entchannel_t so we can check if the FMOD channel still belongs to this entity & entchannel, before checking if it's already playing
|
|
// Special case entchannel -1 => just play locally on listener, no 3D (also override any sound already playing on entity)
|
|
|
|
// System::playSound with paused = true
|
|
result = FMOD_System_PlaySound(fmod_system, sfx->sound, sfx_channelGroup, 1, &channel);
|
|
if (result != FMOD_OK)
|
|
{
|
|
Con_Printf("Failed to play FMOD sound: %s\n", FMOD_ErrorString(result));
|
|
return;
|
|
}
|
|
|
|
// ChannelControl::set3DAttributes
|
|
FMOD_VectorCopy(origin, position);
|
|
FMOD_Channel_Set3DAttributes(channel, &position, NULL);
|
|
FMOD_Channel_SetMode(channel, FMOD_LOOP_OFF); // TODO: some entity sounds do need to loop, e.g. moving elevators. How is this done?
|
|
FMOD_Channel_SetVolume(channel, fvol);
|
|
|
|
// Anything coming from the view entity will always be full volume, and entchannel -1 is used for local sounds (e.g. menu sounds)
|
|
FMOD_Channel_Set3DLevel(channel, entchannel < 0 || entnum == cl.viewentity ? 0.0f : 1.0f);
|
|
|
|
// TODO: attenuation (this is just a quick hack to test)
|
|
entchannel_t *userdata = (entchannel_t*)malloc(sizeof(entchannel_t)); // TODO: we REALLY need to allocate this properly, this is now a memory leak!!
|
|
userdata->entnum = entnum;
|
|
userdata->entchannel = entchannel;
|
|
userdata->channel = channel;
|
|
userdata->dist_mult = attenuation / sound_nominal_clip_dist;
|
|
FMOD_Channel_SetUserData(channel, userdata);
|
|
|
|
// TODO: use ChannelControl Callback to detect when the sound ends, then clear it from the entchannel_t list
|
|
|
|
// Use ChannelControl::setDelay and ChannelControl::getDSPClock to add a delay to move sounds out of phase if necessary
|
|
|
|
// ChannelControl::setPaused = false
|
|
FMOD_Channel_SetPaused(channel, 0);
|
|
|
|
// Store channel handle at position entchannel
|
|
}
|
|
|
|
// TODO: we're still missing Automatic Ambient sounds for water, lava, slime etc (see S_UpdateAmbientSounds)
|
|
void S_StaticSound(sfx_t *sfx, vec3_t origin, float vol, float attenuation) // Note: volume and attenuation are in 0-255 range here
|
|
{
|
|
FMOD_CHANNEL *channel;
|
|
FMOD_VECTOR position;
|
|
FMOD_RESULT result;
|
|
unsigned long long dspclock;
|
|
|
|
if (!fmod_system || !sfx)
|
|
return;
|
|
|
|
S_LoadSound(sfx);
|
|
if (!sfx->sound)
|
|
return;
|
|
|
|
result = FMOD_System_PlaySound(fmod_system, sfx->sound, sfx_channelGroup, 1, &channel);
|
|
if (result != FMOD_OK)
|
|
{
|
|
Con_Printf("Failed to play static FMOD sound: %s\n", FMOD_ErrorString(result));
|
|
return;
|
|
}
|
|
|
|
FMOD_VectorCopy(origin, position);
|
|
FMOD_Channel_Set3DAttributes(channel, &position, NULL);
|
|
FMOD_Channel_SetMode(channel, FMOD_LOOP_NORMAL);
|
|
FMOD_Channel_SetVolume(channel, vol / 255);
|
|
|
|
// Set up attenuation info for use by the rolloff callback
|
|
entchannel_t *userdata = (entchannel_t*)malloc(sizeof(entchannel_t)); // This is rather convenient. Can we use Z_Malloc here perhaps?
|
|
userdata->entnum = -1;
|
|
userdata->channel = channel;
|
|
userdata->dist_mult = (attenuation / 64) / sound_nominal_clip_dist;
|
|
FMOD_Channel_SetUserData(channel, userdata);
|
|
|
|
// Add a random delay so that similar sounds don't phase together
|
|
// Note: this isn't really authentic to the original Quake sound system, but it does improve the sense of directionality
|
|
FMOD_ChannelGroup_GetDSPClock(sfx_channelGroup, &dspclock, NULL);
|
|
FMOD_Channel_SetDelay(channel, dspclock + SND_GetDelay(sfx), 0, 0);
|
|
|
|
FMOD_Channel_SetPaused(channel, 0);
|
|
|
|
// Note: we can forget about the channel we just created, because all SFX channels will be stopped and released on level change through S_StopAllSounds
|
|
}
|
|
|
|
void S_StopSound(int entnum, int entchannel)
|
|
{
|
|
|
|
}
|
|
|
|
void S_StopAllSounds(qboolean clear)
|
|
{
|
|
FMOD_ChannelGroup_Stop(sfx_channelGroup);
|
|
|
|
if (clear)
|
|
S_ClearBuffer();
|
|
}
|
|
|
|
void S_ClearBuffer(void)
|
|
{
|
|
// TODO Use this to remove per-entity channel group and clear all userdata?
|
|
}
|
|
|
|
void S_Update(vec3_t origin, vec3_t forward, vec3_t right, vec3_t up)
|
|
{
|
|
if (!fmod_system)
|
|
return;
|
|
|
|
if (old_volume != sfxvolume.value)
|
|
{
|
|
if (sfxvolume.value < 0)
|
|
Cvar_SetQuick(&sfxvolume, "0");
|
|
else if (sfxvolume.value > 1)
|
|
Cvar_SetQuick(&sfxvolume, "1");
|
|
|
|
old_volume = sfxvolume.value;
|
|
}
|
|
|
|
FMOD_VECTOR fmod_pos, fmod_forward, fmod_up;
|
|
FMOD_VectorCopy(origin, fmod_pos);
|
|
FMOD_VectorCopy(forward, fmod_forward);
|
|
FMOD_VectorCopy(up, fmod_up);
|
|
|
|
// TODO: set listener number based on player ID (for split-screen)
|
|
FMOD_System_Set3DListenerAttributes(fmod_system, 0, &fmod_pos, NULL, &fmod_forward, &fmod_up);
|
|
|
|
FMOD_ChannelGroup_SetVolume(sfx_channelGroup, sfxvolume.value);
|
|
|
|
FMOD_System_Update(fmod_system);
|
|
}
|
|
|
|
void S_ExtraUpdate(void)
|
|
{
|
|
FMOD_System_Update(fmod_system);
|
|
}
|
|
|
|
static void S_SetMasterMute(FMOD_BOOL mute)
|
|
{
|
|
FMOD_CHANNELGROUP *master;
|
|
|
|
if (!fmod_system)
|
|
return;
|
|
|
|
FMOD_System_GetMasterChannelGroup(fmod_system, &master);
|
|
FMOD_ChannelGroup_SetMute(master, mute);
|
|
}
|
|
|
|
void S_BlockSound(void)
|
|
{
|
|
S_SetMasterMute(1);
|
|
}
|
|
|
|
void S_UnblockSound(void)
|
|
{
|
|
S_SetMasterMute(0);
|
|
}
|
|
|
|
sfxcache_t *S_LoadSound(sfx_t *s)
|
|
{
|
|
char namebuffer[256];
|
|
byte *data;
|
|
int len, h;
|
|
FMOD_CREATESOUNDEXINFO exinfo;
|
|
FMOD_RESULT result;
|
|
#if _DEBUG
|
|
FMOD_SOUND_TYPE type;
|
|
FMOD_SOUND_FORMAT format;
|
|
int channels, bits, loopcount;
|
|
#endif
|
|
|
|
if (!fmod_system)
|
|
return NULL;
|
|
|
|
// Check if it's already loaded
|
|
if (s->sound)
|
|
return NULL;
|
|
|
|
q_strlcpy(namebuffer, "sound/", sizeof(namebuffer));
|
|
q_strlcat(namebuffer, s->name, sizeof(namebuffer));
|
|
|
|
// We need to briefly open the file to figure out its length, which FMOD needs to know
|
|
len = COM_OpenFile(namebuffer, &h, NULL);
|
|
if (h >= 0)
|
|
COM_CloseFile(h);
|
|
|
|
data = COM_LoadHunkFile(namebuffer, NULL);
|
|
if (!data)
|
|
{
|
|
Con_Printf("Couldn't load %s\n", namebuffer);
|
|
return NULL;
|
|
}
|
|
|
|
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
|
exinfo.length = len;
|
|
|
|
// System::createSound with FMOD_3D (sfx_t will need a pointer to FMOD_Sound)
|
|
// Might need to set rate and width? FMOD can probably figure this out by itself based on the WAV file contents
|
|
result = FMOD_System_CreateSound(fmod_system, (const char*)data, FMOD_3D | FMOD_OPENMEMORY, &exinfo, &s->sound);
|
|
if (result != FMOD_OK)
|
|
{
|
|
Con_Printf("Failed to create FMOD sound: %s\n", FMOD_ErrorString(result));
|
|
return NULL;
|
|
}
|
|
|
|
FMOD_Sound_GetLength(s->sound, &s->samples, FMOD_TIMEUNIT_PCM);
|
|
|
|
#if _DEBUG
|
|
FMOD_Sound_GetFormat(s->sound, &type, &format, &channels, &bits);
|
|
FMOD_Sound_GetLoopCount(s->sound, &loopcount);
|
|
Con_DPrintf("[FMOD] Loaded sound '%s': type %d, format %d, %d channel(s), %d bits, %d samples, loopcount = %d\n", s->name, type, format, channels, bits, s->samples, loopcount);
|
|
#endif
|
|
|
|
return NULL; // Return value is unused; FMOD has its own internal cache, we never need to use Quake's sfxcache_t
|
|
}
|
|
|
|
void S_TouchSound(const char *sample)
|
|
{
|
|
|
|
}
|
|
|
|
static const char *FMOD_SpeakerModeString(FMOD_SPEAKERMODE speakermode)
|
|
{
|
|
switch (speakermode)
|
|
{
|
|
case FMOD_SPEAKERMODE_MONO:
|
|
return "Mono";
|
|
case FMOD_SPEAKERMODE_STEREO:
|
|
return "Stereo";
|
|
case FMOD_SPEAKERMODE_QUAD:
|
|
return "4.0 Quad";
|
|
case FMOD_SPEAKERMODE_SURROUND:
|
|
return "5.0 Surround";
|
|
case FMOD_SPEAKERMODE_5POINT1:
|
|
return "5.1 Surround";
|
|
case FMOD_SPEAKERMODE_7POINT1:
|
|
return "7.1 Surround";
|
|
case FMOD_SPEAKERMODE_7POINT1POINT4:
|
|
return "7.1.4 Surround";
|
|
default:
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
#endif // USE_FMOD
|