#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 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_NORMAL, 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; } 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) { 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, 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; // 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 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 // 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; } // 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) FMOD_Channel_Set3DLevel(channel, entchannel < 0 || entnum == cl.viewentity ? 0.0f : 1.0f); // 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; // 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) 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) { 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