diff --git a/engine/Quake/q_sound.h b/engine/Quake/q_sound.h index 46b04a4..ca7a385 100644 --- a/engine/Quake/q_sound.h +++ b/engine/Quake/q_sound.h @@ -26,7 +26,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #define __QUAKE_SOUND__ #if USE_FMOD -#include "fmod.h" +typedef struct FMOD_SOUND FMOD_SOUND; #endif /* !!! if this is changed, it must be changed in asm_i386.h too !!! */ @@ -42,6 +42,7 @@ typedef struct sfx_s cache_user_t cache; #if USE_FMOD FMOD_SOUND *sound; + unsigned int samples; #endif } sfx_t; diff --git a/engine/Quake/snd_fmod.c b/engine/Quake/snd_fmod.c index 0073422..911dc0f 100644 --- a/engine/Quake/snd_fmod.c +++ b/engine/Quake/snd_fmod.c @@ -8,17 +8,35 @@ 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 +{ + sfx_t *sfx; + 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, systemrate, numchannels; + int driver, numchannels; char name[1024]; // Create FMOD System if it doesn't exist already @@ -48,7 +66,7 @@ void S_Startup(void) return; } - result = FMOD_System_GetDriverInfo(fmod_system, driver, name, sizeof(name), NULL, &systemrate, &speakermode, &numchannels); + 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)); @@ -56,7 +74,7 @@ void S_Startup(void) } 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), systemrate, numchannels); + (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) @@ -67,6 +85,10 @@ void S_Startup(void) // 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; } @@ -86,29 +108,183 @@ void S_Shutdown(void) } } +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) { + 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 // 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 // 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); + FMOD_Channel_Set3DLevel(channel, entchannel < 0 ? 0.0f : 1.0f); // entchannel -1 is used for local sounds + // TODO: attenuation + + // 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 } -void S_StaticSound(sfx_t *sfx, vec3_t origin, float vol, float attenuation) +// 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; + // Similar to above, but without the per-entity channel group song and dance // Check if sound is looped (should be) and FMOD_Channel_SetMode to looped // Set Channel::setLoopPoints if the sfxcache specifies something non-standard + 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); + + // TODO: attenuation (which is pre-multiplied by 64) + entchannel_t *userdata = (entchannel_t*)malloc(sizeof(entchannel_t)); // This is rather convenient. Can we use Z_Malloc here perhaps? + userdata->sfx = sfx; + 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 original Quake, but it improves 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) @@ -118,7 +294,10 @@ 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) @@ -128,7 +307,28 @@ void S_ClearBuffer(void) void S_Update(vec3_t origin, vec3_t forward, vec3_t right, vec3_t up) { - // System::set3DListenerAttributes + 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); } @@ -170,7 +370,6 @@ sfxcache_t *S_LoadSound(sfx_t *s) FMOD_SOUND_TYPE type; FMOD_SOUND_FORMAT format; int channels, bits, loopcount; - unsigned int length; #endif if (!fmod_system) @@ -208,11 +407,12 @@ sfxcache_t *S_LoadSound(sfx_t *s) 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_GetLength(s->sound, &length, FMOD_TIMEUNIT_MS); FMOD_Sound_GetLoopCount(s->sound, &loopcount); - Con_DPrintf("[FMOD] Loaded sound '%s': type %d, format %d, %d channel(s), %d bits, %d ms, loopcount = %d\n", s->name, type, format, channels, bits, length, 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