Browse Source

Got the FMOD entity sound slot system working in a way that's effective and still fairly simple:

- Up to 1024 entities are statically defined with 8 sound slots each. Sounds played within these ranges will override each other.
- Sounds with free channel assignment get a slot that's dynamically allocated in zone memory.
- Used the channel control callback to detect when a channel is done playing and free the associated slot.
- Also implemented StopSound function
console
Nico de Poel 5 years ago
parent
commit
83210b5822
  1. 202
      engine/Quake/snd_fmod.c

202
engine/Quake/snd_fmod.c

@ -20,15 +20,19 @@ static float SND_FMOD_Attenuation(FMOD_CHANNELCONTROL *channelControl, float dis
#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
typedef struct soundslot_s
{
int entnum;
int entchannel;
float dist_mult;
qboolean zone;
FMOD_CHANNEL *channel;
} entchannel_t;
float dist_mult;
} soundslot_t;
typedef struct entsounds_s
{
soundslot_t slots[8];
} entsounds_t;
static entchannel_t entchannels[MAX_CHANNELS];
static entsounds_t entsounds[MAX_CHANNELS];
void S_Startup(void)
{
@ -87,7 +91,7 @@ void S_Startup(void)
FMOD_ChannelGroup_Set3DMinMaxDistance(sfx_channelGroup, 0.0f, sound_nominal_clip_dist);
FMOD_System_Set3DRolloffCallback(fmod_system, &SND_FMOD_Attenuation);
memset(entchannels, 0, sizeof(entchannels));
memset(entsounds, 0, sizeof(entsounds));
sound_started = true;
}
@ -107,11 +111,11 @@ void S_Shutdown(void)
}
}
static unsigned int SND_GetDelay(const sfx_t *sfx)
static unsigned int SND_GetDelay(const sfx_t *sfx, float maxseconds)
{
unsigned int delay;
delay = 0.1 * fmod_samplerate;
delay = maxseconds * fmod_samplerate;
if (delay > sfx->samples)
delay = sfx->samples;
if (delay > 0)
@ -120,10 +124,18 @@ static unsigned int SND_GetDelay(const sfx_t *sfx)
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;
entchannel_t *userdata;
soundslot_t *userdata;
float scale;
result = FMOD_Channel_GetUserData((FMOD_CHANNEL*)channelcontrol, &userdata);
@ -140,66 +152,78 @@ static float SND_FMOD_Attenuation(FMOD_CHANNELCONTROL *channelcontrol, float dis
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;
//}
/*
=================
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
//return; // HACK play no entity sfx for now
S_LoadSound(sfx);
if (!sfx->sound)
@ -207,8 +231,11 @@ void S_StartSound(int entnum, int entchannel, sfx_t *sfx, vec3_t origin, float f
// 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
// Can use channel group per entity to store entity-specific userdata, with maybe 8-16 fixed channel slots
// Note: entnum <0 is used by temporary entities, means there's no tie to any entity info at all: just play on any free channel
userdata = SND_PickSoundSlot(entnum, entchannel);
// 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
@ -223,7 +250,12 @@ void S_StartSound(int entnum, int entchannel, sfx_t *sfx, vec3_t origin, float f
return;
}
// ChannelControl::set3DAttributes
// 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?
@ -232,30 +264,19 @@ void S_StartSound(int entnum, int entchannel, sfx_t *sfx, vec3_t origin, float f
// 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;
soundslot_t *userdata;
unsigned long long dspclock;
if (!fmod_system || !sfx)
@ -278,16 +299,16 @@ void S_StaticSound(sfx_t *sfx, vec3_t origin, float vol, float attenuation) // N
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 = 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, 0);
FMOD_Channel_SetDelay(channel, dspclock + SND_GetDelay(sfx, 0.2f), 0, 0);
FMOD_Channel_SetPaused(channel, 0);
@ -296,11 +317,28 @@ void S_StaticSound(sfx_t *sfx, vec3_t origin, float vol, float attenuation) // N
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;
// TODO Use this to remove per-entity channel group and clear all userdata? => should already be handled by Stop (but double check!)
FMOD_ChannelGroup_Stop(sfx_channelGroup);
if (clear)
@ -309,7 +347,8 @@ void S_StopAllSounds(qboolean clear)
void S_ClearBuffer(void)
{
// TODO Use this to remove per-entity channel group and clear all userdata?
// 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)
@ -405,8 +444,6 @@ sfxcache_t *S_LoadSound(sfx_t *s)
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)
{
@ -427,7 +464,8 @@ sfxcache_t *S_LoadSound(sfx_t *s)
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)

Loading…
Cancel
Save