From 83210b5822a0063df4d1037bef6bf369f3a426d0 Mon Sep 17 00:00:00 2001 From: Nico de Poel Date: Tue, 13 Apr 2021 12:46:19 +0200 Subject: [PATCH] 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 --- engine/Quake/snd_fmod.c | 202 ++++++++++++++++++++++++---------------- 1 file changed, 120 insertions(+), 82 deletions(-) diff --git a/engine/Quake/snd_fmod.c b/engine/Quake/snd_fmod.c index 34c87e9..249bbe1 100644 --- a/engine/Quake/snd_fmod.c +++ b/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)