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.
 
 
 
 
 

527 lines
14 KiB

#include "quakedef.h"
#ifdef USE_FMOD
#include "fmod.h"
#include "fmod_errors.h"
extern qboolean sound_started; // in snd_dma.c
extern sfx_t *known_sfx;
extern int num_sfx;
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 soundslot_s
{
qboolean zone;
FMOD_CHANNEL *channel;
float dist_mult;
} soundslot_t;
typedef struct entsounds_s
{
soundslot_t slots[8];
} entsounds_t;
static entsounds_t entsounds[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;
}
result = FMOD_System_SetSoftwareChannels(fmod_system, MAX_DYNAMIC_CHANNELS);
if (result != FMOD_OK)
{
Con_Printf("Failed to set number of FMOD software channels: %s\n", FMOD_ErrorString(result));
return;
}
result = FMOD_System_Init(fmod_system, MAX_CHANNELS, FMOD_INIT_VOL0_BECOMES_VIRTUAL, NULL);
if (result != FMOD_OK)
{
Con_Printf("Failed to initialize 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;
}
if (version < FMOD_VERSION)
{
Con_Printf("Incorrect FMOD library version, expected: 0x%x, found: 0x%x", FMOD_VERSION, version);
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(entsounds, 0, sizeof(entsounds));
sound_started = true;
}
void S_Shutdown(void)
{
sfx_t *sfx;
int i;
Con_DPrintf("[FMOD] Shutdown\n");
S_StopAllSounds(true);
// Release all sounds that were loaded and attached to sfx_t's
for (i = 0; i < num_sfx; i++)
{
sfx = &known_sfx[i];
if (sfx->sound)
{
FMOD_Sound_Release(sfx->sound);
sfx->sound = NULL;
}
}
// 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, float maxseconds)
{
unsigned int delay;
delay = maxseconds * fmod_samplerate;
if (delay > sfx->samples)
delay = sfx->samples;
if (delay > 0)
delay = rand() % delay;
return delay;
}
/*
====================
SND_FMOD_Attenuation
Custom rolloff callback that mimics Quake's attenuation algorithm,
whereby sounds can have varying degrees of distance rolloff.
====================
*/
static float SND_FMOD_Attenuation(FMOD_CHANNELCONTROL *channelcontrol, float distance)
{
FMOD_RESULT result;
soundslot_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;
}
/*
=================
SND_FMOD_Callback
Channel control callback that ensures a channel's associated userdata
is properly cleared when the channel stops playing.
=================
*/
static FMOD_RESULT SND_FMOD_Callback(FMOD_CHANNELCONTROL *channelcontrol, FMOD_CHANNELCONTROL_TYPE controltype, FMOD_CHANNELCONTROL_CALLBACK_TYPE callbacktype, void *commanddata1, void *commanddata2)
{
FMOD_RESULT result;
soundslot_t *userdata;
// We're only interested in notifications for when a channel is done playing a sound
if (controltype != FMOD_CHANNELCONTROL_CHANNEL || callbacktype != FMOD_CHANNELCONTROL_CALLBACK_END)
return FMOD_OK;
result = FMOD_Channel_GetUserData((FMOD_CHANNEL*)channelcontrol, &userdata);
if (result != FMOD_OK || !userdata)
{
return FMOD_OK;
}
// Clear the channel to signify that it's free
userdata->channel = NULL;
if (userdata->zone)
{
Z_Free(userdata);
}
return FMOD_OK;
}
static soundslot_t *SND_PickSoundSlot(int entnum, int entchannel)
{
soundslot_t *slot;
if (entnum < 0 || entnum >= MAX_CHANNELS || entchannel == 0 || entchannel > 7)
{
// Just play on any free channel
slot = (soundslot_t*)Z_Malloc(sizeof(soundslot_t));
slot->zone = true;
return slot;
}
// Local sound, use the first slot and override anything already playing
if (entchannel < 0)
entchannel = 0;
slot = &entsounds[entnum].slots[entchannel];
if (slot->channel)
{
// Stop any sound already playing on this slot
FMOD_Channel_Stop(slot->channel);
}
slot->zone = false;
return slot;
}
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;
soundslot_t *userdata;
if (!fmod_system || !sfx)
return;
// TODO: check nosound cvar
S_LoadSound(sfx);
if (!sfx->sound)
return;
// Choose a slot to play the sound on, and stop any conflicting sound on the same entchannel
// Do this before playing the new sound, so that any previous sound will be stopped in time
userdata = SND_PickSoundSlot(entnum, entchannel);
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;
}
// Set up callback data for rolloff and cleanup
userdata->channel = channel;
userdata->dist_mult = attenuation / sound_nominal_clip_dist;
FMOD_Channel_SetUserData(channel, userdata);
FMOD_Channel_SetCallback(channel, &SND_FMOD_Callback);
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)
if (entchannel < 0 || entnum == cl.viewentity)
{
FMOD_Channel_Set3DLevel(channel, 0.0f);
FMOD_Channel_SetPriority(channel, 64); // Ensure local sounds always get priority over other entities
}
// TODO: Use ChannelControl::setDelay and ChannelControl::getDSPClock to add a delay to move sounds out of phase if necessary
FMOD_Channel_SetPaused(channel, 0);
}
// 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;
soundslot_t *userdata;
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
userdata = SND_PickSoundSlot(-1, -1);
userdata->channel = channel;
userdata->dist_mult = (attenuation / 64) / sound_nominal_clip_dist;
FMOD_Channel_SetUserData(channel, userdata);
FMOD_Channel_SetCallback(channel, &SND_FMOD_Callback);
// 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.2f), 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)
{
soundslot_t *slot;
if (!fmod_system)
return;
if (entnum < 0 || entnum >= MAX_CHANNELS || entchannel < 0 || entchannel > 7)
return;
slot = &entsounds[entnum].slots[entchannel];
if (slot->channel)
{
FMOD_Channel_Stop(slot->channel);
}
}
void S_StopAllSounds(qboolean clear)
{
if (!fmod_system)
return;
// Stopping all sounds also ensures that any associated zone memory is freed
FMOD_ChannelGroup_Stop(sfx_channelGroup);
if (clear)
S_ClearBuffer();
}
void S_ClearBuffer(void)
{
// This is meant to prevent the same sound buffer playing over and over while the game is stalled
// I don't think that's really an issue with FMOD
}
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)
{
if (!fmod_system)
return;
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;
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)
{
// Move the sound data up into the CPU cache
// Not really necessary here
}
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