/* ** i_sound.cpp ** System interface for sound; uses fmod.dll ** **--------------------------------------------------------------------------- ** Copyright 1998-2008 Randy Heit ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** */ // HEADER FILES ------------------------------------------------------------ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #include extern HWND Window; #define USE_WINDOWS_DWORD #else #define FALSE 0 #define TRUE 1 #endif #include "templates.h" #include "fmodsound.h" #include "c_cvars.h" #include "i_system.h" #include "w_wad.h" #include "i_music.h" #include "v_text.h" #include "v_palette.h" // MACROS ------------------------------------------------------------------ // killough 2/21/98: optionally use varying pitched sounds #define PITCH(freq,pitch) (snd_pitched ? ((freq)*(pitch))/128.f : float(freq)) // Just some extra for music and whatever #define NUM_EXTRA_SOFTWARE_CHANNELS 1 #define MAX_CHANNELS 256 #define SPECTRUM_SIZE 256 // TYPES ------------------------------------------------------------------- struct FEnumList { const char *Name; int Value; }; // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- FMOD_RESULT SPC_CreateCodec(FMOD::System *sys); // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- static int Enum_NumForName(const FEnumList *list, const char *name); static const char *Enum_NameForNum(const FEnumList *list, int num); // EXTERNAL DATA DECLARATIONS ---------------------------------------------- EXTERN_CVAR (String, snd_output) EXTERN_CVAR (Float, snd_sfxvolume) EXTERN_CVAR (Float, snd_musicvolume) EXTERN_CVAR (Int, snd_buffersize) EXTERN_CVAR (Int, snd_samplerate) EXTERN_CVAR (Bool, snd_pitched) EXTERN_CVAR (Int, snd_channels) extern int sfx_empty; // PUBLIC DATA DEFINITIONS ------------------------------------------------- ReverbContainer *ForcedEnvironment; CVAR (Int, snd_driver, 0, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (Int, snd_buffercount, 0, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (Bool, snd_hrtf, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (Bool, snd_waterreverb, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (String, snd_resampler, "Linear", CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (String, snd_speakermode, "Auto", CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (String, snd_output_format, "PCM-16", CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (String, snd_midipatchset, "", CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (Bool, snd_profile, false, 0) // Underwater low-pass filter cutoff frequency. Set to 0 to disable the filter. CUSTOM_CVAR (Float, snd_waterlp, 250, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) { // Clamp to the DSP unit's limits. if (*self < 10 && *self != 0) { self = 10; } else if (*self > 22000) { self = 22000; } } // PRIVATE DATA DEFINITIONS ------------------------------------------------ static const ReverbContainer *PrevEnvironment; static bool ShowedBanner; // The rolloff callback is called during FMOD::Sound::play, so we need this // global variable to contain the sound info during that time for the // callback. static FRolloffInfo *GRolloff; static float GDistScale; // In the below lists, duplicate entries are for user selection. When // queried, only the first one for the particular value is shown. static const FEnumList OutputNames[] = { { "Auto", FMOD_OUTPUTTYPE_AUTODETECT }, { "Default", FMOD_OUTPUTTYPE_AUTODETECT }, { "No sound", FMOD_OUTPUTTYPE_NOSOUND }, // Windows { "DirectSound", FMOD_OUTPUTTYPE_DSOUND }, { "DSound", FMOD_OUTPUTTYPE_DSOUND }, { "Windows Multimedia", FMOD_OUTPUTTYPE_WINMM }, { "WinMM", FMOD_OUTPUTTYPE_WINMM }, { "WaveOut", FMOD_OUTPUTTYPE_WINMM }, { "OpenAL", FMOD_OUTPUTTYPE_OPENAL }, { "WASAPI", FMOD_OUTPUTTYPE_WASAPI }, { "ASIO", FMOD_OUTPUTTYPE_ASIO }, // Linux { "OSS", FMOD_OUTPUTTYPE_OSS }, { "ALSA", FMOD_OUTPUTTYPE_ALSA }, { "ESD", FMOD_OUTPUTTYPE_ESD }, // Mac { "Sound Manager", FMOD_OUTPUTTYPE_SOUNDMANAGER }, { "Core Audio", FMOD_OUTPUTTYPE_COREAUDIO }, { NULL, 0 } }; static const FEnumList SpeakerModeNames[] = { { "Mono", FMOD_SPEAKERMODE_MONO }, { "Stereo", FMOD_SPEAKERMODE_STEREO }, { "Quad", FMOD_SPEAKERMODE_QUAD }, { "Surround", FMOD_SPEAKERMODE_SURROUND }, { "5.1", FMOD_SPEAKERMODE_5POINT1 }, { "7.1", FMOD_SPEAKERMODE_7POINT1 }, { "Prologic", FMOD_SPEAKERMODE_PROLOGIC }, { "1", FMOD_SPEAKERMODE_MONO }, { "2", FMOD_SPEAKERMODE_STEREO }, { "4", FMOD_SPEAKERMODE_QUAD }, { NULL, 0 } }; static const FEnumList ResamplerNames[] = { { "No Interpolation", FMOD_DSP_RESAMPLER_NOINTERP }, { "NoInterp", FMOD_DSP_RESAMPLER_NOINTERP }, { "Linear", FMOD_DSP_RESAMPLER_LINEAR }, { "Cubic", FMOD_DSP_RESAMPLER_CUBIC }, { "Spline", FMOD_DSP_RESAMPLER_SPLINE }, { NULL, 0 } }; static const FEnumList SoundFormatNames[] = { { "None", FMOD_SOUND_FORMAT_NONE }, { "PCM-8", FMOD_SOUND_FORMAT_PCM8 }, { "PCM-16", FMOD_SOUND_FORMAT_PCM16 }, { "PCM-24", FMOD_SOUND_FORMAT_PCM24 }, { "PCM-32", FMOD_SOUND_FORMAT_PCM32 }, { "PCM-Float", FMOD_SOUND_FORMAT_PCMFLOAT }, { "GCADPCM", FMOD_SOUND_FORMAT_GCADPCM }, { "IMAADPCM", FMOD_SOUND_FORMAT_IMAADPCM }, { "VAG", FMOD_SOUND_FORMAT_VAG }, { "XMA", FMOD_SOUND_FORMAT_XMA }, { "MPEG", FMOD_SOUND_FORMAT_MPEG }, { NULL, 0 } }; static const char *OpenStateNames[] = { "Ready", "Loading", "Error", "Connecting", "Buffering", "Seeking", "Streaming" }; // CODE -------------------------------------------------------------------- //========================================================================== // // Enum_NumForName // // Returns the value of an enum name, or -1 if not found. // //========================================================================== static int Enum_NumForName(const FEnumList *list, const char *name) { while (list->Name != NULL) { if (stricmp(list->Name, name) == 0) { return list->Value; } list++; } return -1; } //========================================================================== // // Enum_NameForNum // // Returns the name of an enum value. If there is more than one name for a // value, on the first one in the list is returned. Returns NULL if there // was no match. // //========================================================================== static const char *Enum_NameForNum(const FEnumList *list, int num) { while (list->Name != NULL) { if (list->Value == num) { return list->Name; } list++; } return NULL; } //========================================================================== // // The container for a streaming FMOD::Sound, for playing music. // //========================================================================== class FMODStreamCapsule : public SoundStream { public: FMODStreamCapsule(FMOD::Sound *stream, FMODSoundRenderer *owner, const char *url) : Owner(owner), Stream(NULL), Channel(NULL), UserData(NULL), Callback(NULL), URL(url), Ended(false) { SetStream(stream); } FMODStreamCapsule(void *udata, SoundStreamCallback callback, FMODSoundRenderer *owner) : Owner(owner), Stream(NULL), Channel(NULL), UserData(udata), Callback(callback), Ended(false) {} ~FMODStreamCapsule() { if (Channel != NULL) { Channel->stop(); } if (Stream != NULL) { Stream->release(); } } void SetStream(FMOD::Sound *stream) { float frequency; Stream = stream; // As this interface is for music, make it super-high priority. if (FMOD_OK == stream->getDefaults(&frequency, NULL, NULL, NULL)) { stream->setDefaults(frequency, 1, 0, 0); } } bool Play(bool looping, float volume) { FMOD_RESULT result; if (URL.IsNotEmpty()) { // Net streams cannot be looped, because they cannot be seeked. looping = false; } Stream->setMode((looping ? FMOD_LOOP_NORMAL : FMOD_LOOP_OFF) | FMOD_SOFTWARE | FMOD_2D); result = Owner->Sys->playSound(FMOD_CHANNEL_FREE, Stream, true, &Channel); if (result != FMOD_OK) { return false; } Channel->setChannelGroup(Owner->MusicGroup); Channel->setSpeakerMix(1, 1, 1, 1, 1, 1, 1, 1); Channel->setVolume(volume); // Ensure reverb is disabled. FMOD_REVERB_CHANNELPROPERTIES reverb = { 0, }; if (FMOD_OK == Channel->getReverbProperties(&reverb)) { reverb.Room = -10000; Channel->setReverbProperties(&reverb); } Channel->setPaused(false); Ended = false; JustStarted = true; Starved = false; Loop = looping; Volume = volume; return true; } void Stop() { if (Channel != NULL) { Channel->stop(); Channel = NULL; } } bool SetPaused(bool paused) { if (Channel != NULL) { return FMOD_OK == Channel->setPaused(paused); } return false; } unsigned int GetPosition() { unsigned int pos; if (Channel != NULL && FMOD_OK == Channel->getPosition(&pos, FMOD_TIMEUNIT_MS)) { return pos; } return 0; } bool IsEnded() { bool is; FMOD_OPENSTATE openstate = FMOD_OPENSTATE_MAX; bool starving; if (Stream == NULL) { return true; } if (FMOD_OK != Stream->getOpenState(&openstate, NULL, &starving)) { openstate = FMOD_OPENSTATE_ERROR; } if (openstate == FMOD_OPENSTATE_ERROR) { if (Channel != NULL) { Channel->stop(); Channel = NULL; } return true; } if (Channel != NULL && (FMOD_OK != Channel->isPlaying(&is) || is == false)) { return true; } if (Ended) { Channel->stop(); Channel = NULL; return true; } if (URL.IsNotEmpty() && !JustStarted && openstate == FMOD_OPENSTATE_READY) { // Reconnect the stream, since it seems to have stalled. // The only way to do this appears to be to completely recreate it. FMOD_RESULT result; Channel->stop(); Stream->release(); Channel = NULL; Stream = NULL; Owner->Sys->setStreamBufferSize(64*1024, FMOD_TIMEUNIT_RAWBYTES); // Open the stream asynchronously, so we don't hang the game while trying to reconnect. // (It would be nice to do the initial open asynchronously as well, but I'd need to rethink // the music system design to pull that off.) result = Owner->Sys->createSound(URL, (Loop ? FMOD_LOOP_NORMAL : FMOD_LOOP_OFF) | FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM | FMOD_NONBLOCKING, NULL, &Stream); JustStarted = true; Owner->Sys->setStreamBufferSize(16*1024, FMOD_TIMEUNIT_RAWBYTES); return result != FMOD_OK; } if (JustStarted && openstate == FMOD_OPENSTATE_STREAMING) { JustStarted = false; } if (JustStarted && Channel == NULL && openstate == FMOD_OPENSTATE_READY) { return !Play(Loop, Volume); } if (starving != Starved) { // Mute the sound if it's starving. Channel->setVolume(starving ? 0 : Volume); Starved = starving; } return false; } void SetVolume(float volume) { if (Channel != NULL && !Starved) { Channel->setVolume(volume); } Volume = volume; } // Sets the position in ms. bool SetPosition(unsigned int ms_pos) { return FMOD_OK == Channel->setPosition(ms_pos, FMOD_TIMEUNIT_MS); } // Sets the order number for MOD formats. bool SetOrder(int order_pos) { return FMOD_OK == Channel->setPosition(order_pos, FMOD_TIMEUNIT_MODORDER); } FString GetStats() { FString stats; FMOD_OPENSTATE openstate; unsigned int percentbuffered; unsigned int position; bool starving; float volume; float frequency; bool paused; bool isplaying; if (FMOD_OK == Stream->getOpenState(&openstate, &percentbuffered, &starving)) { stats = (openstate <= FMOD_OPENSTATE_STREAMING ? OpenStateNames[openstate] : "Unknown state"); stats.AppendFormat(",%3d%% buffered, %s", percentbuffered, starving ? "Starving" : "Well-fed"); } if (Channel == NULL) { stats += ", not playing"; } if (Channel != NULL && FMOD_OK == Channel->getPosition(&position, FMOD_TIMEUNIT_MS)) { stats.AppendFormat(", %d", position); if (FMOD_OK == Stream->getLength(&position, FMOD_TIMEUNIT_MS)) { stats.AppendFormat("/%d", position); } stats += " ms"; } if (Channel != NULL && FMOD_OK == Channel->getVolume(&volume)) { stats.AppendFormat(", %d%%", int(volume * 100)); } if (Channel != NULL && FMOD_OK == Channel->getPaused(&paused) && paused) { stats += ", paused"; } if (Channel != NULL && FMOD_OK == Channel->isPlaying(&isplaying) && isplaying) { stats += ", playing"; } if (Channel != NULL && FMOD_OK == Channel->getFrequency(&frequency)) { stats.AppendFormat(", %g Hz", frequency); } if (JustStarted) { stats += " JS"; } if (Ended) { stats += " XX"; } return stats; } static FMOD_RESULT F_CALLBACK PCMReadCallback(FMOD_SOUND *sound, void *data, unsigned int datalen) { FMOD_RESULT result; FMODStreamCapsule *self; result = ((FMOD::Sound *)sound)->getUserData((void **)&self); if (result != FMOD_OK || self == NULL || self->Callback == NULL || self->Ended) { // Contrary to the docs, this return value is completely ignored. return FMOD_OK; } if (!self->Callback(self, data, datalen, self->UserData)) { self->Ended = true; } return FMOD_OK; } static FMOD_RESULT F_CALLBACK PCMSetPosCallback(FMOD_SOUND *sound, int subsound, unsigned int position, FMOD_TIMEUNIT postype) { // This is useful if the user calls Channel::setPosition and you want // to seek your data accordingly. return FMOD_OK; } private: FMODSoundRenderer *Owner; FMOD::Sound *Stream; FMOD::Channel *Channel; void *UserData; SoundStreamCallback Callback; FString URL; bool Ended; bool JustStarted; bool Starved; bool Loop; float Volume; }; //========================================================================== // // The interface the game uses to talk to FMOD. // //========================================================================== FMODSoundRenderer::FMODSoundRenderer() { InitSuccess = Init(); } FMODSoundRenderer::~FMODSoundRenderer() { Shutdown(); } bool FMODSoundRenderer::IsValid() { return InitSuccess; } //========================================================================== // // FMODSoundRenderer :: Init // //========================================================================== bool FMODSoundRenderer::Init() { FMOD_RESULT result; unsigned int version; FMOD_SPEAKERMODE speakermode; FMOD_SOUND_FORMAT format; FMOD_DSP_RESAMPLER resampler; FMOD_INITFLAGS initflags; int samplerate; int driver; int eval; SFXPaused = 0; DSPLocked = false; MusicGroup = NULL; SfxGroup = NULL; PausableSfx = NULL; SfxConnection = NULL; WaterLP = NULL; WaterReverb = NULL; PrevEnvironment = DefaultEnvironments[0]; DSPClock.AsOne = 0; ChannelGroupTargetUnit = NULL; Printf("I_InitSound: Initializing FMOD\n"); // Create a System object and initialize. result = FMOD::System_Create(&Sys); if (result != FMOD_OK) { Sys = NULL; Printf(TEXTCOLOR_ORANGE"Failed to create FMOD system object: Error %d\n", result); return false; } result = Sys->getVersion(&version); if (result != FMOD_OK) { Printf(TEXTCOLOR_ORANGE"Could not validate FMOD version: Error %d\n", result); return false; } const char *wrongver = NULL; if (version < FMOD_VERSION) { wrongver = "an old"; } else if ((version & 0xFFFF00) > (FMOD_VERSION & 0xFFFF00)) { wrongver = "a new"; } if (wrongver != NULL) { Printf (" "TEXTCOLOR_ORANGE"Error! You are using %s version of FMOD (%x.%02x.%02x).\n" " "TEXTCOLOR_ORANGE"This program requires version %x.%02x.%02x\n", wrongver, version >> 16, (version >> 8) & 255, version & 255, FMOD_VERSION >> 16, (FMOD_VERSION >> 8) & 255, FMOD_VERSION & 255); return false; } if (!ShowedBanner) { Printf("FMOD Sound System, copyright © Firelight Technologies Pty, Ltd., 1994-2008.\n"); ShowedBanner = true; } #ifdef _WIN32 if (OSPlatform == os_WinNT4) { // The following was true as of FMOD 3. I don't know if it still // applies to FMOD Ex, nor do I have an NT 4 install anymore, but // there's no reason to get rid of it yet. // // If running Windows NT 4, we need to initialize DirectSound before // using WinMM. If we don't, then FSOUND_Close will corrupt a // heap. This might just be the Audigy's drivers--I don't know why // it happens. At least the fix is simple enough. I only need to // initialize DirectSound once, and then I can initialize/close // WinMM as many times as I want. // // Yes, using WinMM under NT 4 is a good idea. I can get latencies as // low as 20 ms with WinMM, but with DirectSound I need to have the // latency as high as 120 ms to avoid crackling--quite the opposite // from the other Windows versions with real DirectSound support. static bool inited_dsound = false; if (!inited_dsound) { if (Sys->setOutput(FMOD_OUTPUTTYPE_DSOUND) == FMOD_OK) { if (Sys->init(1, FMOD_INIT_NORMAL, 0) == FMOD_OK) { inited_dsound = true; Sleep(50); Sys->close(); } Sys->setOutput(FMOD_OUTPUTTYPE_WINMM); } } } #endif // Set the user specified output mode. eval = Enum_NumForName(OutputNames, snd_output); if (eval >= 0) { result = Sys->setOutput(FMOD_OUTPUTTYPE(eval)); if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE"Setting output type '%s' failed. Using default instead. (Error %d)\n", *snd_output, result); Sys->setOutput(FMOD_OUTPUTTYPE_AUTODETECT); } } result = Sys->getNumDrivers(&driver); if (result == FMOD_OK) { if (snd_driver >= driver) { Printf(TEXTCOLOR_BLUE"Driver %d does not exist. Using 0.\n", *snd_driver); driver = 0; } else { driver = snd_driver; } result = Sys->setDriver(driver); } result = Sys->getDriver(&driver); result = Sys->getDriverCaps(driver, &Driver_Caps, &Driver_MinFrequency, &Driver_MaxFrequency, &speakermode); if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE"Could not ascertain driver capabilities. Some things may be weird. (Error %d)\n", result); // Fill in some default to pretend it worked. (But as long as we specify a valid driver, // can this call actually fail?) Driver_Caps = 0; Driver_MinFrequency = 4000; Driver_MaxFrequency = 48000; speakermode = FMOD_SPEAKERMODE_STEREO; } // Set the user selected speaker mode. eval = Enum_NumForName(SpeakerModeNames, snd_speakermode); if (eval >= 0) { speakermode = FMOD_SPEAKERMODE(eval); } result = Sys->setSpeakerMode(speakermode); if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE"Could not set speaker mode to '%s'. (Error %d)\n", *snd_speakermode, result); } // Set software format eval = Enum_NumForName(SoundFormatNames, snd_output_format); format = eval >= 0 ? FMOD_SOUND_FORMAT(eval) : FMOD_SOUND_FORMAT_PCM16; eval = Enum_NumForName(ResamplerNames, snd_resampler); resampler = eval >= 0 ? FMOD_DSP_RESAMPLER(eval) : FMOD_DSP_RESAMPLER_LINEAR; samplerate = clamp(snd_samplerate, Driver_MinFrequency, Driver_MaxFrequency); if (samplerate == 0 || snd_samplerate == 0) { // Creative's ASIO drivers report the only supported frequency as 0! if (FMOD_OK != Sys->getSoftwareFormat(&samplerate, NULL, NULL, NULL, NULL, NULL)) { samplerate = 48000; } } if (samplerate != snd_samplerate && snd_samplerate != 0) { Printf(TEXTCOLOR_BLUE"Sample rate %d is unsupported. Trying %d.\n", *snd_samplerate, samplerate); } result = Sys->setSoftwareFormat(samplerate, format, 0, 0, resampler); if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE"Could not set mixing format. Defaults will be used. (Error %d)\n", result); } // Set software channels according to snd_channels result = Sys->setSoftwareChannels(snd_channels + NUM_EXTRA_SOFTWARE_CHANNELS); if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE"Failed to set the preferred number of channels. (Error %d)\n", result); } if (Driver_Caps & FMOD_CAPS_HARDWARE_EMULATED) { // The user has the 'Acceleration' slider set to off! // This is really bad for latency! Printf (TEXTCOLOR_BLUE"Warning: The sound acceleration slider has been set to off.\n"); Printf (TEXTCOLOR_BLUE"Please turn it back on if you want decent sound.\n"); result = Sys->setDSPBufferSize(1024, 10); // At 48khz, the latency between issuing an fmod command and hearing it will now be about 213ms. } else if (snd_buffersize != 0 || snd_buffercount != 0) { int buffersize = snd_buffersize ? snd_buffersize : 1024; int buffercount = snd_buffercount ? snd_buffercount : 4; result = Sys->setDSPBufferSize(buffersize, buffercount); } else { result = FMOD_OK; } if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE"Setting DSP buffer size failed. (Error %d)\n", result); } // Try to init initflags = FMOD_INIT_NORMAL; if (snd_hrtf) { initflags |= FMOD_INIT_SOFTWARE_HRTF; } if (snd_profile) { initflags |= FMOD_INIT_ENABLE_PROFILE; } for (;;) { result = Sys->init(snd_channels + NUM_EXTRA_SOFTWARE_CHANNELS, initflags, 0); if (result == FMOD_ERR_OUTPUT_CREATEBUFFER) { // Possible causes of a buffer creation failure: // 1. The speaker mode selected isn't supported by this soundcard. Force it to stereo. // 2. The output format is unsupported. Force it to 16-bit PCM. // 3. ??? result = Sys->getSpeakerMode(&speakermode); if (result == FMOD_OK && speakermode != FMOD_SPEAKERMODE_STEREO && FMOD_OK == Sys->setSpeakerMode(FMOD_SPEAKERMODE_STEREO)) { Printf(TEXTCOLOR_RED" Buffer creation failed. Retrying with stereo output.\n"); continue; } result = Sys->getSoftwareFormat(&samplerate, &format, NULL, NULL, &resampler, NULL); if (result == FMOD_OK && format != FMOD_SOUND_FORMAT_PCM16 && FMOD_OK == Sys->setSoftwareFormat(samplerate, FMOD_SOUND_FORMAT_PCM16, 0, 0, resampler)) { Printf(TEXTCOLOR_RED" Buffer creation failed. Retrying with PCM-16 output.\n"); continue; } } #ifdef _WIN32 else if (result == FMOD_ERR_OUTPUT_INIT) { FMOD_OUTPUTTYPE output; result = Sys->getOutput(&output); if (result == FMOD_OK && output != FMOD_OUTPUTTYPE_DSOUND) { Printf(TEXTCOLOR_BLUE" Init failed for output type %s. Retrying with DirectSound.\n", Enum_NameForNum(OutputNames, output)); if (FMOD_OK == Sys->setOutput(FMOD_OUTPUTTYPE_DSOUND)) { continue; } } } #endif break; } if (result != FMOD_OK) { // Initializing FMOD failed. Cry cry. Printf(TEXTCOLOR_ORANGE" System::init returned error code %d\n", result); return false; } // Create channel groups result = Sys->createChannelGroup("Music", &MusicGroup); if (result != FMOD_OK) { Printf(TEXTCOLOR_ORANGE" Could not create music channel group. (Error %d)\n", result); return false; } result = Sys->createChannelGroup("SFX", &SfxGroup); if (result != FMOD_OK) { Printf(TEXTCOLOR_ORANGE" Could not create sfx channel group. (Error %d)\n", result); return false; } result = Sys->createChannelGroup("Pausable SFX", &PausableSfx); if (result != FMOD_OK) { Printf(TEXTCOLOR_ORANGE" Could not create pausable sfx channel group. (Error %d)\n", result); return false; } result = SfxGroup->addGroup(PausableSfx); if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE" Could not attach pausable sfx to sfx channel group. (Error %d)\n", result); } // Create DSP units for underwater effect result = Sys->createDSPByType(FMOD_DSP_TYPE_LOWPASS, &WaterLP); if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE" Could not create underwater lowpass unit. (Error %d)\n", result); } else { result = Sys->createDSPByType(FMOD_DSP_TYPE_REVERB, &WaterReverb); if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE" Could not create underwater reverb unit. (Error %d)\n", result); } } // Connect underwater DSP unit between PausableSFX and SFX groups, while // retaining the connection established by SfxGroup->addGroup(). if (WaterLP != NULL) { FMOD::DSP *sfx_head, *pausable_head; result = SfxGroup->getDSPHead(&sfx_head); result = sfx_head->getInput(0, &pausable_head, &SfxConnection); result = WaterLP->addInput(pausable_head, NULL); WaterLP->setActive(false); WaterLP->setParameter(FMOD_DSP_LOWPASS_CUTOFF, snd_waterlp); WaterLP->setParameter(FMOD_DSP_LOWPASS_RESONANCE, 2); if (WaterReverb != NULL) { FMOD::DSPConnection *dry; result = WaterReverb->addInput(pausable_head, &dry); result = dry->setMix(0.1f); result = WaterReverb->addInput(WaterLP, NULL); result = sfx_head->addInput(WaterReverb, NULL); WaterReverb->setParameter(FMOD_DSP_REVERB_ROOMSIZE, 0.001f); WaterReverb->setParameter(FMOD_DSP_REVERB_DAMP, 0.2f); WaterReverb->setActive(false); } else { result = sfx_head->addInput(WaterLP, NULL); } } LastWaterLP = snd_waterlp; // Find the FMOD Channel Group Target Unit. To completely eliminate sound // while the program is deactivated, we can deactivate this DSP unit, and // all audio processing will cease. This is not directly exposed by the // API but can be easily located by getting the master channel group and // tracing its single output, since it is known to hook up directly to the // Channel Group Target Unit. (See FMOD Profiler for proof.) FMOD::ChannelGroup *master_group; result = Sys->getMasterChannelGroup(&master_group); if (result == FMOD_OK) { FMOD::DSP *master_head; result = master_group->getDSPHead(&master_head); if (result == FMOD_OK) { result = master_head->getOutput(0, &ChannelGroupTargetUnit, NULL); if (result != FMOD_OK) { ChannelGroupTargetUnit = NULL; } } } result = SPC_CreateCodec(Sys); if (result != FMOD_OK) { Printf(TEXTCOLOR_BLUE" Could not register SPC codec. (Error %d)\n", result); } if (FMOD_OK != Sys->getSoftwareFormat(&OutputRate, NULL, NULL, NULL, NULL, NULL)) { OutputRate = 48000; // Guess, but this should never happen. } Sys->set3DSettings(0.5f, 96.f, 1.f); Sys->set3DRolloffCallback(RolloffCallback); snd_sfxvolume.Callback (); return true; } //========================================================================== // // FMODSoundRenderer :: Shutdown // //========================================================================== void FMODSoundRenderer::Shutdown() { if (Sys != NULL) { if (MusicGroup != NULL) { MusicGroup->release(); MusicGroup = NULL; } if (PausableSfx != NULL) { PausableSfx->release(); PausableSfx = NULL; } if (SfxGroup != NULL) { SfxGroup->release(); SfxGroup = NULL; } if (WaterLP != NULL) { WaterLP->release(); WaterLP = NULL; } if (WaterReverb != NULL) { WaterReverb->release(); WaterReverb = NULL; } Sys->close(); Sys->release(); Sys = NULL; } } //========================================================================== // // FMODSoundRenderer :: GetOutputRate // //========================================================================== float FMODSoundRenderer::GetOutputRate() { return (float)OutputRate; } //========================================================================== // // FMODSoundRenderer :: PrintStatus // //========================================================================== void FMODSoundRenderer::PrintStatus() { FMOD_OUTPUTTYPE output; FMOD_SPEAKERMODE speakermode; FMOD_SOUND_FORMAT format; FMOD_DSP_RESAMPLER resampler; int driver; int samplerate; int numoutputchannels; int num2d, num3d, total; unsigned int bufferlength; int numbuffers; if (FMOD_OK == Sys->getOutput(&output)) { Printf ("Output type: "TEXTCOLOR_GREEN"%s\n", Enum_NameForNum(OutputNames, output)); } if (FMOD_OK == Sys->getSpeakerMode(&speakermode)) { Printf ("Speaker mode: "TEXTCOLOR_GREEN"%s\n", Enum_NameForNum(SpeakerModeNames, speakermode)); } if (FMOD_OK == Sys->getDriver(&driver)) { char name[256]; if (FMOD_OK != Sys->getDriverInfo(driver, name, sizeof(name), NULL)) { strcpy(name, "Unknown"); } Printf ("Driver: "TEXTCOLOR_GREEN"%d"TEXTCOLOR_NORMAL" ("TEXTCOLOR_ORANGE"%s"TEXTCOLOR_NORMAL")\n", driver, name); DumpDriverCaps(Driver_Caps, Driver_MinFrequency, Driver_MaxFrequency); } if (FMOD_OK == Sys->getHardwareChannels(&num2d, &num3d, &total)) { Printf (TEXTCOLOR_YELLOW "Hardware 2D channels: "TEXTCOLOR_GREEN"%d\n", num2d); Printf (TEXTCOLOR_YELLOW "Hardware 3D channels: "TEXTCOLOR_GREEN"%d\n", num3d); Printf (TEXTCOLOR_YELLOW "Total hardware channels: "TEXTCOLOR_GREEN"%d\n", total); } if (FMOD_OK == Sys->getSoftwareFormat(&samplerate, &format, &numoutputchannels, NULL, &resampler, NULL)) { Printf (TEXTCOLOR_LIGHTBLUE "Software mixer sample rate: "TEXTCOLOR_GREEN"%d\n", samplerate); Printf (TEXTCOLOR_LIGHTBLUE "Software mixer format: "TEXTCOLOR_GREEN"%s\n", Enum_NameForNum(SoundFormatNames, format)); Printf (TEXTCOLOR_LIGHTBLUE "Software mixer channels: "TEXTCOLOR_GREEN"%d\n", numoutputchannels); Printf (TEXTCOLOR_LIGHTBLUE "Software mixer resampler: "TEXTCOLOR_GREEN"%s\n", Enum_NameForNum(ResamplerNames, resampler)); } if (FMOD_OK == Sys->getDSPBufferSize(&bufferlength, &numbuffers)) { Printf (TEXTCOLOR_LIGHTBLUE "DSP buffers: "TEXTCOLOR_GREEN"%u samples x %d\n", bufferlength, numbuffers); } } //========================================================================== // // FMODSoundRenderer :: DumpDriverCaps // //========================================================================== void FMODSoundRenderer::DumpDriverCaps(FMOD_CAPS caps, int minfrequency, int maxfrequency) { Printf (TEXTCOLOR_OLIVE " Min. frequency: "TEXTCOLOR_GREEN"%d\n", minfrequency); Printf (TEXTCOLOR_OLIVE " Max. frequency: "TEXTCOLOR_GREEN"%d\n", maxfrequency); Printf (" Features:\n"); if (caps == 0) Printf(TEXTCOLOR_OLIVE " None\n"); if (caps & FMOD_CAPS_HARDWARE) Printf(TEXTCOLOR_OLIVE " Hardware mixing\n"); if (caps & FMOD_CAPS_HARDWARE_EMULATED) Printf(TEXTCOLOR_OLIVE " Hardware acceleration is turned off!\n"); if (caps & FMOD_CAPS_OUTPUT_MULTICHANNEL) Printf(TEXTCOLOR_OLIVE " Multichannel\n"); if (caps & FMOD_CAPS_OUTPUT_FORMAT_PCM8) Printf(TEXTCOLOR_OLIVE " PCM-8"); if (caps & FMOD_CAPS_OUTPUT_FORMAT_PCM16) Printf(TEXTCOLOR_OLIVE " PCM-16"); if (caps & FMOD_CAPS_OUTPUT_FORMAT_PCM24) Printf(TEXTCOLOR_OLIVE " PCM-24"); if (caps & FMOD_CAPS_OUTPUT_FORMAT_PCM32) Printf(TEXTCOLOR_OLIVE " PCM-32"); if (caps & FMOD_CAPS_OUTPUT_FORMAT_PCMFLOAT) Printf(TEXTCOLOR_OLIVE " PCM-Float"); if (caps & (FMOD_CAPS_OUTPUT_FORMAT_PCM8 | FMOD_CAPS_OUTPUT_FORMAT_PCM16 | FMOD_CAPS_OUTPUT_FORMAT_PCM24 | FMOD_CAPS_OUTPUT_FORMAT_PCM32 | FMOD_CAPS_OUTPUT_FORMAT_PCMFLOAT)) { Printf("\n"); } if (caps & FMOD_CAPS_REVERB_EAX2) Printf(TEXTCOLOR_OLIVE " EAX2"); if (caps & FMOD_CAPS_REVERB_EAX3) Printf(TEXTCOLOR_OLIVE " EAX3"); if (caps & FMOD_CAPS_REVERB_EAX4) Printf(TEXTCOLOR_OLIVE " EAX4"); if (caps & FMOD_CAPS_REVERB_EAX5) Printf(TEXTCOLOR_OLIVE " EAX5"); if (caps & FMOD_CAPS_REVERB_I3DL2) Printf(TEXTCOLOR_OLIVE " I3DL2"); if (caps & (FMOD_CAPS_REVERB_EAX2 | FMOD_CAPS_REVERB_EAX3 | FMOD_CAPS_REVERB_EAX4 | FMOD_CAPS_REVERB_EAX5 | FMOD_CAPS_REVERB_I3DL2)) { Printf("\n"); } if (caps & FMOD_CAPS_REVERB_LIMITED) Printf("TEXTCOLOR_OLIVE Limited reverb\n"); } //========================================================================== // // FMODSoundRenderer :: PrintDriversList // //========================================================================== void FMODSoundRenderer::PrintDriversList() { int numdrivers; int i; char name[256]; if (FMOD_OK == Sys->getNumDrivers(&numdrivers)) { for (i = 0; i < numdrivers; ++i) { if (FMOD_OK == Sys->getDriverInfo(i, name, sizeof(name), NULL)) { Printf("%d. %s\n", i, name); } } } } //========================================================================== // // FMODSoundRenderer :: GatherStats // //========================================================================== FString FMODSoundRenderer::GatherStats() { int channels; float dsp, stream, update, total; FString out; channels = 0; total = update = stream = dsp = 0; Sys->getChannelsPlaying(&channels); Sys->getCPUUsage(&dsp, &stream, &update, &total); out.Format ("%d channels,"TEXTCOLOR_YELLOW"%5.2f"TEXTCOLOR_NORMAL"%% CPU " "(DSP:"TEXTCOLOR_YELLOW"%2.2f"TEXTCOLOR_NORMAL"%% " "Stream:"TEXTCOLOR_YELLOW"%2.2f"TEXTCOLOR_NORMAL"%% " "Update:"TEXTCOLOR_YELLOW"%2.2f"TEXTCOLOR_NORMAL"%%)", channels, total, dsp, stream, update); return out; } //========================================================================== // // FMODSoundRenderer :: SetSfxVolume // //========================================================================== void FMODSoundRenderer::SetSfxVolume(float volume) { SfxGroup->setVolume(volume); } //========================================================================== // // FMODSoundRenderer :: SetMusicVolume // //========================================================================== void FMODSoundRenderer::SetMusicVolume(float volume) { MusicGroup->setVolume(volume); } //========================================================================== // // FMODSoundRenderer :: CreateStream // // Creates a streaming sound that receives PCM data through a callback. // //========================================================================== SoundStream *FMODSoundRenderer::CreateStream (SoundStreamCallback callback, int buffbytes, int flags, int samplerate, void *userdata) { FMODStreamCapsule *capsule; FMOD::Sound *sound; FMOD_RESULT result; FMOD_CREATESOUNDEXINFO exinfo = { sizeof(exinfo), }; FMOD_MODE mode; int sample_shift; int channel_shift; capsule = new FMODStreamCapsule (userdata, callback, this); mode = FMOD_2D | FMOD_OPENUSER | FMOD_LOOP_NORMAL | FMOD_SOFTWARE | FMOD_CREATESTREAM | FMOD_OPENONLY; sample_shift = (flags & (SoundStream::Bits32 | SoundStream::Float)) ? 2 : (flags & SoundStream::Bits8) ? 0 : 1; channel_shift = (flags & SoundStream::Mono) ? 0 : 1; // Chunk size of stream update in samples. This will be the amount of data // passed to the user callback. exinfo.decodebuffersize = buffbytes >> (sample_shift + channel_shift); // Number of channels in the sound. exinfo.numchannels = 1 << channel_shift; // Length of PCM data in bytes of whole song (for Sound::getLength). // This pretends it's extremely long. exinfo.length = ~0u; // Default playback rate of sound. */ exinfo.defaultfrequency = samplerate; // Data format of sound. if (flags & SoundStream::Float) { exinfo.format = FMOD_SOUND_FORMAT_PCMFLOAT; } else if (flags & SoundStream::Bits32) { exinfo.format = FMOD_SOUND_FORMAT_PCM32; } else if (flags & SoundStream::Bits8) { exinfo.format = FMOD_SOUND_FORMAT_PCM8; } else { exinfo.format = FMOD_SOUND_FORMAT_PCM16; } // User callback for reading. exinfo.pcmreadcallback = FMODStreamCapsule::PCMReadCallback; // User callback for seeking. exinfo.pcmsetposcallback = FMODStreamCapsule::PCMSetPosCallback; // User data to be attached to the sound during creation. Access via Sound::getUserData. exinfo.userdata = capsule; result = Sys->createSound(NULL, mode, &exinfo, &sound); if (result != FMOD_OK) { delete capsule; return NULL; } capsule->SetStream(sound); return capsule; } //========================================================================== // // FMODSoundRenderer :: OpenStream // // Creates a streaming sound from a file on disk. // //========================================================================== SoundStream *FMODSoundRenderer::OpenStream(const char *filename_or_data, int flags, int offset, int length) { FMOD_MODE mode; FMOD_CREATESOUNDEXINFO exinfo = { sizeof(exinfo), }; FMOD::Sound *stream; FMOD_RESULT result; bool url; mode = FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM; if (flags & SoundStream::Loop) { mode |= FMOD_LOOP_NORMAL; } if (offset == -1) { mode |= FMOD_OPENMEMORY; offset = 0; } exinfo.length = length; exinfo.fileoffset = offset; if ((*snd_midipatchset)[0] != '\0') { exinfo.dlsname = snd_midipatchset; } url = (offset == 0 && length == 0 && strstr(filename_or_data, "://") > filename_or_data); if (url) { // Use a larger buffer for URLs so that it's less likely to be effected // by hiccups in the data rate from the remote server. Sys->setStreamBufferSize(64*1024, FMOD_TIMEUNIT_RAWBYTES); } result = Sys->createSound(filename_or_data, mode, &exinfo, &stream); if (url) { // Restore standard buffer size. Sys->setStreamBufferSize(16*1024, FMOD_TIMEUNIT_RAWBYTES); } if (result == FMOD_ERR_FORMAT && exinfo.dlsname != NULL) { // FMOD_ERR_FORMAT could refer to either the main sound file or // to the DLS instrument set. Try again without special DLS // instruments to see if that lets it succeed. exinfo.dlsname = NULL; result = Sys->createSound(filename_or_data, mode, &exinfo, &stream); if (result == FMOD_OK) { Printf("%s is an unsupported format.\n", *snd_midipatchset); } } if (result == FMOD_OK) { return new FMODStreamCapsule(stream, this, url ? filename_or_data : NULL); } return NULL; } //========================================================================== // // FMODSoundRenderer :: StartSound // //========================================================================== FSoundChan *FMODSoundRenderer::StartSound(SoundHandle sfx, float vol, int pitch, int chanflags, FSoundChan *reuse_chan) { FMOD_RESULT result; FMOD_MODE mode; FMOD::Channel *chan; float freq; if (FMOD_OK == ((FMOD::Sound *)sfx.data)->getDefaults(&freq, NULL, NULL, NULL)) { freq = PITCH(freq, pitch); } else { freq = 0; } GRolloff = NULL; // Do 2D sounds need rolloff? result = Sys->playSound(FMOD_CHANNEL_FREE, (FMOD::Sound *)sfx.data, true, &chan); if (FMOD_OK == result) { result = chan->getMode(&mode); if (result != FMOD_OK) { assert(0); mode = FMOD_SOFTWARE; } mode = (mode & ~FMOD_3D) | FMOD_2D; if (chanflags & CHAN_LOOP) { mode = (mode & ~FMOD_LOOP_OFF) | FMOD_LOOP_NORMAL; } chan->setMode(mode); chan->setChannelGroup((chanflags & (CHAN_UI | CHAN_NOPAUSE)) ? SfxGroup : PausableSfx); if (freq != 0) { chan->setFrequency(freq); } chan->setVolume(vol); HandleChannelDelay(chan, reuse_chan, freq); chan->setPaused(false); return CommonChannelSetup(chan, reuse_chan); } //DPrintf ("Sound %s failed to play: %d\n", sfx->name.GetChars(), result); return NULL; } //========================================================================== // // FMODSoundRenderer :: StartSound3D // //========================================================================== CVAR(Float, snd_3dspread, 180, 0) FSoundChan *FMODSoundRenderer::StartSound3D(SoundHandle sfx, SoundListener *listener, float vol, FRolloffInfo *rolloff, float distscale, int pitch, int priority, const FVector3 &pos, const FVector3 &vel, int channum, int chanflags, FSoundChan *reuse_chan) { FMOD_RESULT result; FMOD_MODE mode; FMOD::Channel *chan; float freq; float def_freq, def_vol, def_pan; int numchans; int def_priority; if (FMOD_OK == ((FMOD::Sound *)sfx.data)->getDefaults(&def_freq, &def_vol, &def_pan, &def_priority)) { freq = PITCH(def_freq, pitch); // Change the sound's default priority before playing it. ((FMOD::Sound *)sfx.data)->setDefaults(def_freq, def_vol, def_pan, clamp(def_priority - priority, 1, 256)); } else { freq = 0; def_priority = -1; } // Play it. GRolloff = rolloff; GDistScale = distscale; // Experiments indicate that playSound will ignore priorities and always succeed // as long as the paremeters are set properly. It will first try to kick out sounds // with the same priority level but has no problem with kicking out sounds at // higher priority levels if it needs to. result = Sys->playSound(FMOD_CHANNEL_FREE, (FMOD::Sound *)sfx.data, true, &chan); // Then set the priority back. if (def_priority >= 0) { ((FMOD::Sound *)sfx.data)->setDefaults(def_freq, def_vol, def_pan, def_priority); } // Reduce volume of stereo sounds, because each channel will be summed together // and is likely to be very similar, resulting in an amplitude twice what it // would have been had it been mixed to mono. if (FMOD_OK == ((FMOD::Sound *)sfx.data)->getFormat(NULL, NULL, &numchans, NULL)) { if (numchans > 1) { vol *= 0.5f; } } if (FMOD_OK == result) { result = chan->getMode(&mode); if (result != FMOD_OK) { mode = FMOD_3D | FMOD_SOFTWARE; } if (chanflags & CHAN_LOOP) { mode = (mode & ~FMOD_LOOP_OFF) | FMOD_LOOP_NORMAL; } mode = SetChanHeadSettings(listener, chan, pos, channum, chanflags, mode); chan->setMode(mode); chan->setChannelGroup((chanflags & (CHAN_UI | CHAN_NOPAUSE)) ? SfxGroup : PausableSfx); if (freq != 0) { chan->setFrequency(freq); } chan->setVolume(vol); if (mode & FMOD_3D) { chan->set3DAttributes((FMOD_VECTOR *)&pos[0], (FMOD_VECTOR *)&vel[0]); chan->set3DSpread(snd_3dspread); } HandleChannelDelay(chan, reuse_chan, freq); chan->setPaused(false); FSoundChan *schan = CommonChannelSetup(chan, reuse_chan); schan->Rolloff = *rolloff; return schan; } GRolloff = NULL; //DPrintf ("Sound %s failed to play: %d\n", sfx->name.GetChars(), result); return 0; } //========================================================================== // // FMODSoundRenderer :: HandleChannelDelay // // If the sound is restarting, seek it to its proper place. // Otherwise, record its starting time. // //========================================================================== void FMODSoundRenderer::HandleChannelDelay(FMOD::Channel *chan, FSoundChan *reuse_chan, float freq) const { if (reuse_chan != NULL) { // Sound is being restarted, so seek it to the position // it would be in now if it had never been evicted. QWORD_UNION nowtime; chan->getDelay(FMOD_DELAYTYPE_DSPCLOCK_START, &nowtime.Hi, &nowtime.Lo); // If CHAN_ABSTIME is set, the sound is being restored, and // the channel's start time is actually its seek position. if (reuse_chan->ChanFlags & CHAN_ABSTIME) { unsigned int seekpos = reuse_chan->StartTime.Lo; if (seekpos > 0) { chan->setPosition(seekpos, FMOD_TIMEUNIT_PCM); } reuse_chan->StartTime.AsOne = QWORD(nowtime.AsOne - seekpos * OutputRate / freq); reuse_chan->ChanFlags &= ~CHAN_ABSTIME; } else { QWORD difftime = nowtime.AsOne - reuse_chan->StartTime.AsOne; if (difftime > 0) { chan->setPosition((unsigned int)(difftime / OutputRate), FMOD_TIMEUNIT_MS); } } } else { chan->setDelay(FMOD_DELAYTYPE_DSPCLOCK_START, DSPClock.Hi, DSPClock.Lo); } } //========================================================================== // // FMODSoundRenderer :: SetChanHeadSettings // // If this sound is played at the same coordinates as the listener, make // it head relative. Also, area sounds should use no 3D panning if close // enough to the listener. // //========================================================================== FMOD_MODE FMODSoundRenderer::SetChanHeadSettings(SoundListener *listener, FMOD::Channel *chan, const FVector3 &pos, int channum, int chanflags, FMOD_MODE oldmode) const { if (!listener->valid) { return oldmode; } FVector3 cpos, mpos; cpos = listener->position; if (chanflags & CHAN_AREA) { float level, old_level; // How far are we from the perceived sound origin? Within a certain // short distance, we interpolate between 2D panning and full 3D panning. const double interp_range = 32.0; double dist_sqr = (cpos - pos).LengthSquared(); if (dist_sqr == 0) { level = 0; } else if (dist_sqr <= interp_range * interp_range) { // Within interp_range: Interpolate between none and full 3D panning. level = float(1 - (interp_range - sqrt(dist_sqr)) / interp_range); } else { // Beyond interp_range: Normal 3D panning. level = 1; } if (chan->get3DPanLevel(&old_level) == FMOD_OK && old_level != level) { // Only set it if it's different. chan->set3DPanLevel(level); if (level < 1) { // Let the noise come from all speakers, not just the front ones. // A centered 3D sound does not play at full volume, so neither should the 2D-panned one. // This is sqrt(0.5), which is the result for a centered equal power panning. chan->setSpeakerMix(0.70711f,0.70711f,0.70711f,0.70711f,0.70711f,0.70711f,0.70711f,0.70711f); } } return oldmode; } else if (cpos == pos) { // Head relative return (oldmode & ~FMOD_3D) | FMOD_2D; } // World relative return (oldmode & ~FMOD_2D) | FMOD_3D; } //========================================================================== // // FMODSoundRenderer :: CommonChannelSetup // // Assign an end callback to the channel and allocates a game channel for // it. // //========================================================================== FSoundChan *FMODSoundRenderer::CommonChannelSetup(FMOD::Channel *chan, FSoundChan *reuse_chan) const { FSoundChan *schan; if (reuse_chan != NULL) { schan = reuse_chan; schan->ChanFlags &= ~CHAN_EVICTED; schan->SysChannel = chan; } else { schan = S_GetChannel(chan); chan->getDelay(FMOD_DELAYTYPE_DSPCLOCK_START, &schan->StartTime.Hi, &schan->StartTime.Lo); } chan->setUserData(schan); chan->setCallback(FMOD_CHANNEL_CALLBACKTYPE_END, ChannelEndCallback, 0); GRolloff = NULL; return schan; } //========================================================================== // // FMODSoundRenderer :: StopSound // //========================================================================== void FMODSoundRenderer::StopSound(FSoundChan *chan) { if (chan == NULL) return; if (chan->SysChannel != NULL) { // S_EvictAllChannels() will set the CHAN_EVICTED flag to indicate // that it wants to keep all the channel information around. if (!(chan->ChanFlags & CHAN_EVICTED)) { chan->ChanFlags |= CHAN_FORGETTABLE; } ((FMOD::Channel *)chan->SysChannel)->stop(); } else { S_ReturnChannel(chan); } } //========================================================================== // // FMODSoundRenderer :: GetPosition // // Returns position of sound on this channel, in samples. // //========================================================================== unsigned int FMODSoundRenderer::GetPosition(FSoundChan *chan) { unsigned int pos; if (chan == NULL || chan->SysChannel == NULL) { return 0; } ((FMOD::Channel *)chan->SysChannel)->getPosition(&pos, FMOD_TIMEUNIT_PCM); return pos; } //========================================================================== // // FMODSoundRenderer :: SetSfxPaused // //========================================================================== void FMODSoundRenderer::SetSfxPaused(bool paused, int slot) { int oldslots = SFXPaused; if (paused) { SFXPaused |= 1 << slot; } else { SFXPaused &= ~(1 << slot); } //Printf("%d\n", SFXPaused); if (oldslots != 0 && SFXPaused == 0) { PausableSfx->setPaused(false); } else if (oldslots == 0 && SFXPaused != 0) { PausableSfx->setPaused(true); } } //========================================================================== // // FMODSoundRenderer :: SetInactive // // This is similar to SetSfxPaused but will *pause* everything, including // the global reverb effect. This is meant to be used only when the // game is deactivated, not for general sound pausing. // //========================================================================== void FMODSoundRenderer::SetInactive(bool inactive) { if (ChannelGroupTargetUnit != NULL) { ChannelGroupTargetUnit->setActive(!inactive); } } //========================================================================== // // FMODSoundRenderer :: UpdateSoundParams3D // //========================================================================== void FMODSoundRenderer::UpdateSoundParams3D(SoundListener *listener, FSoundChan *chan, const FVector3 &pos, const FVector3 &vel) { if (chan == NULL || chan->SysChannel == NULL) return; FMOD::Channel *fchan = (FMOD::Channel *)chan->SysChannel; FMOD_MODE oldmode, mode; if (fchan->getMode(&oldmode) != FMOD_OK) { oldmode = FMOD_3D | FMOD_SOFTWARE; } mode = SetChanHeadSettings(listener, fchan, pos, chan->EntChannel, chan->ChanFlags, oldmode); if (mode != oldmode) { // Only set the mode if it changed. fchan->setMode(mode); } fchan->set3DAttributes((FMOD_VECTOR *)&pos[0], (FMOD_VECTOR *)&vel[0]); } //========================================================================== // // FMODSoundRenderer :: UpdateListener // //========================================================================== void FMODSoundRenderer::UpdateListener(SoundListener *listener) { FMOD_VECTOR pos, vel; FMOD_VECTOR forward; FMOD_VECTOR up; if (!listener->valid) { return; } // Set velocity to 0 to prevent crazy doppler shifts just from running. vel.x = listener->velocity.X; vel.z = listener->velocity.Y; vel.y = listener->velocity.Z; pos.x = listener->position.X; pos.z = listener->position.Y; pos.y = listener->position.Z; float angle = listener->angle; forward.x = cos(angle); forward.y = 0; forward.z = sin(angle); up.x = 0; up.y = 1; up.z = 0; Sys->set3DListenerAttributes(0, &pos, &vel, &forward, &up); bool underwater = false; const ReverbContainer *env; if (ForcedEnvironment) { env = ForcedEnvironment; } else { underwater = (listener->underwater && snd_waterlp); env = listener->Environment; if (env == NULL) { env = DefaultEnvironments[0]; } } if (env != PrevEnvironment || env->Modified) { DPrintf ("Reverb Environment %s\n", env->Name); const_cast(env)->Modified = false; Sys->setReverbProperties((FMOD_REVERB_PROPERTIES *)(&env->Properties)); PrevEnvironment = env; } if (underwater || env->SoftwareWater) { //PausableSfx->setPitch(0.64171f); // This appears to be what Duke 3D uses PausableSfx->setPitch(0.7937005f); // Approx. 4 semitones lower; what Nash suggested if (WaterLP != NULL) { if (LastWaterLP != snd_waterlp) { LastWaterLP = snd_waterlp; WaterLP->setParameter(FMOD_DSP_LOWPASS_CUTOFF, snd_waterlp); } WaterLP->setActive(true); if (WaterReverb != NULL && snd_waterreverb) { WaterReverb->setActive(true); WaterReverb->setBypass(false); SfxConnection->setMix(0); } else { // Let some of the original mix through so that high frequencies are // not completely lost. The reverb unit has its own connection and // preserves dry sounds itself if used. SfxConnection->setMix(0.1f); if (WaterReverb != NULL) { WaterReverb->setActive(true); WaterReverb->setBypass(true); } } } } else { PausableSfx->setPitch(1); if (WaterLP != NULL) { SfxConnection->setMix(1); WaterLP->setActive(false); if (WaterReverb != NULL) { WaterReverb->setActive(false); } } } } //========================================================================== // // FMODSoundRenderer :: Sync // // Used by the save/load code to restart sounds at the same position they // were in at the time of saving. Must not be nested. // //========================================================================== void FMODSoundRenderer::Sync(bool sync) { DSPLocked = sync; if (sync) { Sys->lockDSP(); Sys->getDSPClock(&DSPClock.Hi, &DSPClock.Lo); } else { Sys->unlockDSP(); } } //========================================================================== // // FMODSoundRenderer :: UpdateSounds // //========================================================================== void FMODSoundRenderer::UpdateSounds() { // Any sounds played between now and the next call to this function // will start exactly one tic from now. Sys->getDSPClock(&DSPClock.Hi, &DSPClock.Lo); DSPClock.AsOne += OutputRate / TICRATE; Sys->update(); } //========================================================================== // // FMODSoundRenderer :: LoadSoundRaw // //========================================================================== SoundHandle FMODSoundRenderer::LoadSoundRaw(BYTE *sfxdata, int length, int frequency, int channels, int bits) { FMOD_CREATESOUNDEXINFO exinfo = { sizeof(exinfo), }; SoundHandle retval = { NULL }; if (length == 0) return retval; exinfo.length = length; exinfo.numchannels = channels; exinfo.defaultfrequency = frequency; switch (bits) { case 8: // Need to convert sample data from unsigned to signed. for (int i = 0; i < length; ++i) { sfxdata[i] = sfxdata[i] - 128; } case -8: exinfo.format = FMOD_SOUND_FORMAT_PCM8; break; case 16: exinfo.format = FMOD_SOUND_FORMAT_PCM16; break; case 32: exinfo.format = FMOD_SOUND_FORMAT_PCM32; break; default: return retval; } const FMOD_MODE samplemode = FMOD_3D | FMOD_OPENMEMORY | FMOD_SOFTWARE | FMOD_OPENRAW; FMOD::Sound *sample; FMOD_RESULT result; result = Sys->createSound((char *)sfxdata, samplemode, &exinfo, &sample); if (result != FMOD_OK) { DPrintf("Failed to allocate sample: Error %d\n", result); return retval; } retval.data = sample; return retval; } //========================================================================== // // FMODSoundRenderer :: LoadSound // //========================================================================== SoundHandle FMODSoundRenderer::LoadSound(BYTE *sfxdata, int length) { FMOD_CREATESOUNDEXINFO exinfo = { sizeof(exinfo), }; SoundHandle retval = { NULL }; if (length == 0) return retval; exinfo.length = length; const FMOD_MODE samplemode = FMOD_3D | FMOD_OPENMEMORY | FMOD_SOFTWARE; FMOD::Sound *sample; FMOD_RESULT result; result = Sys->createSound((char *)sfxdata, samplemode, &exinfo, &sample); if (result != FMOD_OK) { DPrintf("Failed to allocate sample: Error %d\n", result); return retval; } retval.data = sample; return retval; } //========================================================================== // // FMODSoundRenderer :: UnloadSound // //========================================================================== void FMODSoundRenderer::UnloadSound(SoundHandle sfx) { if (sfx.data != NULL) { ((FMOD::Sound *)sfx.data)->release(); } } //========================================================================== // // FMODSoundRenderer :: GetMSLength // //========================================================================== unsigned int FMODSoundRenderer::GetMSLength(SoundHandle sfx) { if (sfx.data != NULL) { unsigned int length; if (((FMOD::Sound *)sfx.data)->getLength(&length, FMOD_TIMEUNIT_MS) == FMOD_OK) { return length; } } return 0; // Don't know. } //========================================================================== // // FMODSoundRenderer :: ChannelEndCallback static // // Called when the channel finishes playing. // //========================================================================== FMOD_RESULT F_CALLBACK FMODSoundRenderer::ChannelEndCallback (FMOD_CHANNEL *channel, FMOD_CHANNEL_CALLBACKTYPE type, int cmd, unsigned int data1, unsigned int data2) { assert(type == FMOD_CHANNEL_CALLBACKTYPE_END); FMOD::Channel *chan = (FMOD::Channel *)channel; FSoundChan *schan; if (chan->getUserData((void **)&schan) == FMOD_OK && schan != NULL) { bool evicted; // If the sound was stopped with GSnd->StopSound(), then we know // it wasn't evicted. Otherwise, if it's looping, it must have // been evicted. If it's not looping, then it was evicted if it // didn't reach the end of its playback. if (schan->ChanFlags & CHAN_FORGETTABLE) { evicted = false; } else if (schan->ChanFlags & (CHAN_LOOP | CHAN_EVICTED)) { evicted = true; } else { FMOD::Sound *sound; unsigned int len, pos; evicted = false; // Assume not evicted if (FMOD_OK == chan->getPosition(&pos, FMOD_TIMEUNIT_PCM)) { // If position is 0, then this sound either didn't have // a chance to play at all, or it stopped normally. if (pos == 0) { if (schan->ChanFlags & CHAN_JUSTSTARTED) { evicted = true; } } else if (FMOD_OK == chan->getCurrentSound(&sound)) { if (FMOD_OK == sound->getLength(&len, FMOD_TIMEUNIT_PCM)) { if (pos < len) { evicted = true; } } } } } if (!evicted) { S_ReturnChannel(schan); } else { schan->ChanFlags |= CHAN_EVICTED; schan->SysChannel = NULL; } } return FMOD_OK; } //========================================================================== // // FMODSoundRenderer :: RolloffCallback static // // Calculates a volume for the sound based on distance. // //========================================================================== float F_CALLBACK FMODSoundRenderer::RolloffCallback(FMOD_CHANNEL *channel, float distance) { FMOD::Channel *chan = (FMOD::Channel *)channel; FSoundChan *schan; FRolloffInfo *rolloff; if (GRolloff != NULL) { rolloff = GRolloff; distance *= GDistScale; } else if (chan->getUserData((void **)&schan) == FMOD_OK && schan != NULL) { rolloff = &schan->Rolloff; distance *= schan->DistanceScale; } else { return 0; } if (rolloff == NULL) { return 0; } if (distance <= rolloff->MinDistance) { return 1; } if (rolloff->RolloffType == ROLLOFF_Log) { // Logarithmic rolloff has no max distance where it goes silent. return rolloff->MinDistance / (rolloff->MinDistance + rolloff->RolloffFactor * (distance - rolloff->MinDistance)); } if (distance >= rolloff->MaxDistance) { return 0; } float volume = (rolloff->MaxDistance - distance) / (rolloff->MaxDistance - rolloff->MinDistance); if (rolloff->RolloffType == ROLLOFF_Custom && S_SoundCurve != NULL) { volume = S_SoundCurve[int(S_SoundCurveSize * (1 - volume))] / 127.f; } if (rolloff->RolloffType == ROLLOFF_Linear) { return volume; } else { return (powf(10.f, volume) - 1.f) / 9.f; } } //========================================================================== // // FMODSoundRenderer :: DrawWaveDebug // // Bit 0: ( 1) Show oscilloscope for sfx. // Bit 1: ( 2) Show spectrum for sfx. // Bit 2: ( 4) Show oscilloscope for music. // Bit 3: ( 8) Show spectrum for music. // Bit 4: (16) Show oscilloscope for all sounds. // Bit 5: (32) Show spectrum for all sounds. // //========================================================================== void FMODSoundRenderer::DrawWaveDebug(int mode) { const int window_height = 100; float wavearray[MAXWIDTH]; int window_size; int numoutchans; int y; if (FMOD_OK != Sys->getSoftwareFormat(NULL, NULL, &numoutchans, NULL, NULL, NULL)) { return; } // Scale all the channel windows so one group fits completely on one row, with // 16 pixels of padding between each window. window_size = (screen->GetWidth() - 16) / numoutchans - 16; y = 16; y = DrawChannelGroupOutput(SfxGroup, wavearray, window_size, window_height, y, mode); y = DrawChannelGroupOutput(MusicGroup, wavearray, window_size, window_height, y, mode >> 2); y = DrawSystemOutput(wavearray, window_size, window_height, y, mode >> 4); } //========================================================================== // // FMODSoundRenderer :: DrawChannelGroupOutput // // Draws an oscilloscope and/or a spectrum for a channel group. // //========================================================================== int FMODSoundRenderer::DrawChannelGroupOutput(FMOD::ChannelGroup *group, float *wavearray, int width, int height, int y, int mode) { int y1, y2; switch (mode & 0x03) { case 0x01: // Oscilloscope only return DrawChannelGroupWaveData(group, wavearray, width, height, y, false); case 0x02: // Spectrum only return DrawChannelGroupSpectrum(group, wavearray, width, height, y, false); case 0x03: // Oscilloscope + Spectrum width = (width + 16) / 2 - 16; y1 = DrawChannelGroupSpectrum(group, wavearray, width, height, y, true); y2 = DrawChannelGroupWaveData(group, wavearray, width, height, y, true); return MAX(y1, y2); } return y; } //========================================================================== // // FMODSoundRenderer :: DrawSystemOutput // // Like DrawChannelGroupOutput(), but uses the system object. // //========================================================================== int FMODSoundRenderer::DrawSystemOutput(float *wavearray, int width, int height, int y, int mode) { int y1, y2; switch (mode & 0x03) { case 0x01: // Oscilloscope only return DrawSystemWaveData(wavearray, width, height, y, false); case 0x02: // Spectrum only return DrawSystemSpectrum(wavearray, width, height, y, false); case 0x03: // Oscilloscope + Spectrum width = (width + 16) / 2 - 16; y1 = DrawSystemSpectrum(wavearray, width, height, y, true); y2 = DrawSystemWaveData(wavearray, width, height, y, true); return MAX(y1, y2); } return y; } //========================================================================== // // FMODSoundRenderer :: DrawChannelGroupWaveData // // Draws all the output channels for a specified channel group. // Setting skip to true causes it to skip every other window. // //========================================================================== int FMODSoundRenderer::DrawChannelGroupWaveData(FMOD::ChannelGroup *group, float *wavearray, int width, int height, int y, bool skip) { int drawn = 0; int x = 16; while (FMOD_OK == group->getWaveData(wavearray, width, drawn)) { drawn++; DrawWave(wavearray, x, y, width, height); x += (width + 16) << int(skip); } if (drawn) { y += height + 16; } return y; } //========================================================================== // // FMODSoundRenderer::DrawSystemWaveData // // Like DrawChannelGroupWaveData, but it uses the system object to get the // complete output. // //========================================================================== int FMODSoundRenderer::DrawSystemWaveData(float *wavearray, int width, int height, int y, bool skip) { int drawn = 0; int x = 16; while (FMOD_OK == Sys->getWaveData(wavearray, width, drawn)) { drawn++; DrawWave(wavearray, x, y, width, height); x += (width + 16) << int(skip); } if (drawn) { y += height + 16; } return y; } //========================================================================== // // FMODSoundRenderer :: DrawWave // // Draws an oscilloscope at the specified coordinates on the screen. Each // entry in the wavearray buffer has its own column. IOW, there are // entries in wavearray. // //========================================================================== void FMODSoundRenderer::DrawWave(float *wavearray, int x, int y, int width, int height) { float scale = height / 2.f; float mid = y + scale; int i; // Draw a box around the oscilloscope. screen->DrawLine(x - 1, y - 1, x + width, y - 1, -1, MAKEARGB(160, 0, 40, 200)); screen->DrawLine(x + width + 1, y - 1, x + width, y + height, -1, MAKEARGB(160, 0, 40, 200)); screen->DrawLine(x + width, y + height, x - 1, y + height, -1, MAKEARGB(160, 0, 40, 200)); screen->DrawLine(x - 1, y + height, x - 1, y - 1, -1, MAKEARGB(160, 0, 40, 200)); // Draw the actual oscilloscope. if (screen->Accel2D) { // Drawing this with lines is super-slow without hardware acceleration, at least with // the debug build. float lasty = mid - wavearray[0] * scale; float newy; for (i = 1; i < width; ++i) { newy = mid - wavearray[i] * scale; screen->DrawLine(x + i - 1, int(lasty), x + i, int(newy), -1, MAKEARGB(255,255,248,248)); lasty = newy; } } else { for (i = 0; i < width; ++i) { float y = wavearray[i] * scale + mid; screen->DrawPixel(x + i, int(y), -1, MAKEARGB(255,255,255,255)); } } } //========================================================================== // // FMODSoundRenderer :: DrawChannelGroupSpectrum // // Draws all the spectrum for a specified channel group. // Setting skip to true causes it to skip every other window, starting at // the second one. // //========================================================================== int FMODSoundRenderer::DrawChannelGroupSpectrum(FMOD::ChannelGroup *group, float *spectrumarray, int width, int height, int y, bool skip) { int drawn = 0; int x = 16; if (skip) { x += width + 16; } while (FMOD_OK == group->getSpectrum(spectrumarray, SPECTRUM_SIZE, drawn, FMOD_DSP_FFT_WINDOW_TRIANGLE)) { drawn++; DrawSpectrum(spectrumarray, x, y, width, height); x += (width + 16) << int(skip); } if (drawn) { y += height + 16; } return y; } //========================================================================== // // FMODSoundRenderer::DrawSystemSpectrum // // Like DrawChannelGroupSpectrum, but it uses the system object to get the // complete output. // //========================================================================== int FMODSoundRenderer::DrawSystemSpectrum(float *spectrumarray, int width, int height, int y, bool skip) { int drawn = 0; int x = 16; if (skip) { x += width + 16; } while (FMOD_OK == Sys->getSpectrum(spectrumarray, SPECTRUM_SIZE, drawn, FMOD_DSP_FFT_WINDOW_TRIANGLE)) { drawn++; DrawSpectrum(spectrumarray, x, y, width, height); x += (width + 16) << int(skip); } if (drawn) { y += height + 16; } return y; } //========================================================================== // // FMODSoundRenderer :: DrawSpectrum // // Draws a spectrum at the specified coordinates on the screen. // //========================================================================== void FMODSoundRenderer::DrawSpectrum(float *spectrumarray, int x, int y, int width, int height) { float scale = height / 2.f; float mid = y + scale; float db; int top; // Draw a border and dark background for the spectrum. screen->DrawLine(x - 1, y - 1, x + width, y - 1, -1, MAKEARGB(160, 0, 40, 200)); screen->DrawLine(x + width + 1, y - 1, x + width, y + height, -1, MAKEARGB(160, 0, 40, 200)); screen->DrawLine(x + width, y + height, x - 1, y + height, -1, MAKEARGB(160, 0, 40, 200)); screen->DrawLine(x - 1, y + height, x - 1, y - 1, -1, MAKEARGB(160, 0, 40, 200)); screen->Dim(MAKERGB(0,0,0), 0.3f, x, y, width, height); // Draw the actual spectrum. for (int i = 0; i < width; ++i) { db = spectrumarray[i * (SPECTRUM_SIZE - 2) / width + 1]; db = MAX(-150.f, 10 * log10f(db) * 2); // Convert to decibels and clamp db = 1.f - (db / -150.f); db *= height; top = (int)db; if (top >= height) { top = height - 1; } // screen->Clear(x + i, int(y + height - db), x + i + 1, y + height, -1, MAKEARGB(255, 255, 255, 40)); screen->Dim(MAKERGB(255,255,40), 0.65f, x + i, y + height - top, 1, top); } } //========================================================================== // // FMODSoundRenderer :: DecodeSample // // Uses FMOD to decode a compressed sample to a 16-bit buffer. This is used // by the DUMB XM reader to handle FMOD's OggMods. // //========================================================================== short *FMODSoundRenderer::DecodeSample(int outlen, const void *coded, int sizebytes, ECodecType type) { FMOD_CREATESOUNDEXINFO exinfo = { sizeof(exinfo), }; FMOD::Sound *sound; FMOD_SOUND_FORMAT format; int channels; unsigned int len, amt_read; FMOD_RESULT result; short *outbuf; if (type == CODEC_Vorbis) { exinfo.suggestedsoundtype = FMOD_SOUND_TYPE_OGGVORBIS; } exinfo.length = sizebytes; result = Sys->createSound((const char *)coded, FMOD_2D | FMOD_SOFTWARE | FMOD_CREATESTREAM | FMOD_OPENMEMORY_POINT | FMOD_OPENONLY | FMOD_LOWMEM, &exinfo, &sound); if (result != FMOD_OK) { return NULL; } result = sound->getFormat(NULL, &format, &channels, NULL); // TODO: Handle more formats if it proves necessary. // Does OggMod work with stereo samples? if (result != FMOD_OK || format != FMOD_SOUND_FORMAT_PCM16 || channels != 1) { sound->release(); return NULL; } len = outlen; // Must be malloc'ed for DUMB, which is C. outbuf = (short *)malloc(len); result = sound->readData(outbuf, len, &amt_read); sound->release(); if (result == FMOD_ERR_FILE_EOF) { memset((BYTE *)outbuf + amt_read, 0, len - amt_read); } else if (result != FMOD_OK || amt_read != len) { free(outbuf); return NULL; } return outbuf; }