/* * Copyright (C) Volition, Inc. 1999. All rights reserved. * * All source code herein is the property of Volition, Inc. You may not sell * or otherwise commercially exploit the source or things you created based on the * source. * */ #include "cfile/cfile.h" #include "cmdline/cmdline.h" #include "debugconsole/console.h" #include "gamesnd/eventmusic.h" #include "gamesnd/gamesnd.h" #include "globalincs/alphacolors.h" #include "globalincs/pstypes.h" #include "globalincs/vmallocator.h" #include "menuui/mainhallmenu.h" #include "mod_table/mod_table.h" #include "options/Option.h" #include "osapi/osapi.h" #include "render/3d.h" #include "sound/audiostr.h" #include "sound/ds.h" #include "sound/ds3d.h" #include "sound/dscap.h" #include "tracing/Monitor.h" #include "tracing/tracing.h" #ifdef WITH_FFMPEG #include "sound/ffmpeg/FFmpegWaveFile.h" #endif #include #define SND_F_USED (1<<0) // Sounds[] element is used struct loaded_sound { int sid; // software id char filename[MAX_FILENAME_LEN]; int sig; int flags; sound_info info; int uncompressed_size; // size (in bytes) of sound (uncompressed) int duration; }; SCP_vector Sounds; int Sound_enabled = FALSE; // global flag to turn sound on/off size_t Snd_sram; // mem (in bytes) used up by storing sounds in system memory float Default_sound_volume = 1.0f; // range is 0 -> 1, used for non-music sound fx float Master_sound_volume = Default_sound_volume; static bool effects_volume_change_listener(float new_val, bool /*initial*/) { Assertion(new_val >= 0.0f && new_val <= 1.0f, "Invalid value %f supplied by options system!", new_val); snd_set_effects_volume(new_val); return true; } static void parse_effect_volume_func() { float volume; stuff_float(&volume); CLAMP(volume, 0.0f, 1.0f); Default_sound_volume = volume; } // coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton static auto EffectVolumeOption __UNUSED = options::OptionBuilder("Audio.Effects", std::pair{"Effects", 1370}, std::pair{"Volume used for playing in-game effects", 1734}) .category(std::make_pair("Audio", 1826)) .default_func([](){ return Default_sound_volume; }) .range(0.0f, 1.0f) .change_listener(effects_volume_change_listener) .importance(2) .flags({options::OptionFlags::RetailBuiltinOption}) .parser(parse_effect_volume_func) .finish(); float Default_voice_volume = 0.7f; // range is 0 -> 1, used for all voice playback float Master_voice_volume = Default_voice_volume; static bool voice_volume_change_listener(float new_val, bool /*initial*/) { Assertion(new_val >= 0.0f && new_val <= 1.0f, "Invalid value %f supplied by options system!", new_val); snd_set_voice_volume(new_val); return true; } static void parse_voice_volume_func() { float volume; stuff_float(&volume); CLAMP(volume, 0.0f, 1.0f); Default_voice_volume = volume; } // coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton static auto VoiceVolumeOption __UNUSED = options::OptionBuilder("Audio.Voice", std::pair{"Voice", 1372}, std::pair{"Volume used for playing voice audio", 1735}) .category(std::make_pair("Audio", 1826)) .default_func([]() { return Default_voice_volume; }) .range(0.0f, 1.0f) .change_listener(voice_volume_change_listener) .importance(0) .flags({options::OptionFlags::RetailBuiltinOption}) .parser(parse_voice_volume_func) .finish(); unsigned int SND_ENV_DEFAULT = 0; struct LoopingSoundInfo { float m_defaultVolume; //!< The default volume of this sound (from game_snd) float m_dynamicVolume; //!< The dynamic volume before scripted volume adjustment is applied (is updated via snd_set_volume) LoopingSoundInfo(float defaultVolume, float dynamicVolume) : m_defaultVolume(defaultVolume), m_dynamicVolume(dynamicVolume) {} LoopingSoundInfo() : m_defaultVolume(0.0f), m_dynamicVolume(0.0f) {} }; SCP_unordered_map currentlyLoopingSoundInfos; SCP_unordered_map currentlyLooping3dSoundInfos; //For the adjust-audio-volume sexp float aav_music_volume = 1.0f; float aav_voice_volume = 1.0f; float aav_effect_volume = 1.0f; // these must correspond to the order of the AAV_ #defines static float* aav_volume[] = { &aav_music_volume, &aav_voice_volume, &aav_effect_volume }; struct aav { float start_volume; float delta; float start_time; int delta_time; }; aav aav_data[3]; // min volume to play a sound after all volume processing (range is 0.0 -> 1.0) #define MIN_SOUND_VOLUME 0.005f static int snd_next_sig = 1; // convert the game level sound priorities to the DirectSound priority descriptions int ds_priority(int priority) { switch(priority){ case SND_PRIORITY_MUST_PLAY: return DS_MUST_PLAY; case SND_PRIORITY_SINGLE_INSTANCE: return DS_LIMIT_ONE; case SND_PRIORITY_DOUBLE_INSTANCE: return DS_LIMIT_TWO; case SND_PRIORITY_TRIPLE_INSTANCE: return DS_LIMIT_THREE; default: Int3(); return DS_MUST_PLAY; }; } void snd_clear() { Sounds.clear(); // reset how much storage sounds are taking up in memory Snd_sram = 0; } // --------------------------------------------------------------------------------------- // Initialize the game sound system // // Initialize the game sound system. Depending on what sound library is being used, // call the appropriate low-level initiailizations // // returns: 1 => init success // 0 => init failed // int snd_init() { int rval; if ( Cmdline_freespace_no_sound ) return 0; if (ds_initialized) { nprintf(( "Sound", "SOUND => Audio is already initialized!\n" )); return 1; } snd_clear(); rval = ds_init(); if ( rval != 0 ) { nprintf(( "Sound", "SOUND ==> Fatal error initializing audio device, turn sound off.\n" )); Cmdline_freespace_no_sound = Cmdline_freespace_no_music = 1; goto Failure; } snd_aav_init(); // Init the audio streaming stuff audiostream_init(); ds_initialized = 1; Sound_enabled = TRUE; return 1; Failure: // Warning(LOCATION, "Sound system was unable to be initialized. If you continue, sound will be disabled.\n"); nprintf(( "Sound", "SOUND => Audio init unsuccessful, continuing without sound.\n" )); return 0; } int Sound_spew = 0; DCF(show_sounds, "Toggles display of sound debug info") { if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { dc_printf("Sound debug info is %s", (Sound_spew ? "ON" : "OFF")); return; } Sound_spew = !Sound_spew; dc_printf("Sound debug info is %s", (Sound_spew ? "ON" : "OFF")); } extern SCP_vector Snds; extern SCP_vector Snds_iface; void snd_spew_debug_info() { int game_sounds = 0; int message_sounds = 0; int interface_sounds = 0; int done = 0; if(!Sound_spew){ return; } // count up game, interface and message sounds for (size_t idx = 0; idx < Sounds.size(); idx++) { if(!(Sounds[idx].flags & SND_F_USED)){ continue; } done = 0; // what kind of sound is this for(SCP_vector::iterator gs = Snds.begin(); gs != Snds.end(); ++gs){ for (auto& entry : gs->sound_entries) { if(!stricmp(entry.filename, Sounds[idx].filename)){ game_sounds++; done = 1; } } } if(!done){ for(SCP_vector::iterator gs = Snds.begin(); gs != Snds.end(); ++gs){ for (auto& entry : gs->sound_entries) { if(!stricmp(entry.filename, Sounds[idx].filename)) { interface_sounds++; done = 1; } } } } if(!done){ message_sounds++; } } // spew info int line_height = gr_get_font_height() + 1; int sx = gr_screen.center_offset_x + 30; int sy = gr_screen.center_offset_y + 100; gr_set_color_fast(&Color_normal); gr_printf_no_resize(sx, sy, "Game sounds : %d\n", game_sounds); sy += line_height; gr_printf_no_resize(sx, sy, "Interface sounds : %d\n", interface_sounds); sy += line_height; gr_printf_no_resize(sx, sy, "Message sounds : %d\n", message_sounds); sy += line_height; gr_printf_no_resize(sx, sy, "Total sounds : %d\n", game_sounds + interface_sounds + message_sounds); } static std::unique_ptr openAudioFile(const char* fileName) { #ifdef WITH_FFMPEG { std::unique_ptr audio_file(new sound::ffmpeg::FFmpegWaveFile()); if (audio_file->Open(fileName, false)) { return audio_file; } } #endif return nullptr; } // --------------------------------------------------------------------------------------- // snd_load() // // Load a sound into memory and prepare it for playback. The sound will reside in memory as // a single instance, and can be played multiple times simultaneously. Through the magic of // DirectSound, only 1 copy of the sound is used. // // parameters: entry => entry of sound to load // parameters: flags => pointer to flags of sound to load, so they // can be modified if necessary; can be nullptr // allow_hardware_load => whether to try to allocate in hardware // // returns: success => index of sound in Sounds[] array // failure => -1 // //int snd_load( char *filename, int hardware, int use_ds3d, int *sig) sound_load_id snd_load(game_snd_entry* entry, int *flags, int /*allow_hardware_load*/) { int type; sound_info* si; loaded_sound* snd; size_t n; if (!ds_initialized) return sound_load_id::invalid(); if (flags && *flags & GAME_SND_NOT_VALID) return sound_load_id::invalid(); if (!VALID_FNAME(entry->filename)) { if (flags) *flags |= GAME_SND_NOT_VALID; return sound_load_id::invalid(); } for (n = 0; n < Sounds.size(); n++) { if ( !(Sounds[n].flags & SND_F_USED) ) { break; } else if ( !stricmp( Sounds[n].filename, entry->filename) ) { // extra check: make sure the sound is actually loaded in a compatible way (2D vs. 3D) // // NOTE: this will allow a duplicate 3D entry if 2D stereo entry exists, // but will not load a duplicate 2D entry to get stereo if 3D // version already loaded if ( (Sounds[n].info.n_channels == 1) || !(flags && *flags & GAME_SND_USE_DS3D) ) { // Check we need to populate the signature here, as modders may desire to use // a sound file more than once in their tables. if (entry->id_sig == -1) { entry->id_sig = Sounds[n].sig; } return sound_load_id(static_cast(n)); } } } if ( n == Sounds.size() ) { loaded_sound new_sound; new_sound.sid = -1; new_sound.flags = 0; Sounds.push_back(new_sound); } snd = &Sounds[n]; si = &snd->info; TRACE_SCOPE(tracing::LoadSound); nprintf(("Sound", "SOUND ==> Loading '%s'\n", entry->filename)); std::unique_ptr audio_file = openAudioFile(entry->filename); if (audio_file == nullptr) { if (flags) *flags |= GAME_SND_NOT_VALID; return sound_load_id::invalid(); } auto fileProps = audio_file->getFileProperties(); type = 0; if (flags && *flags & GAME_SND_USE_DS3D) { type |= DS_3D; if (fileProps.num_channels > 1) { // We need to resample the audio down to one channel sound::ResampleProperties resample; resample.num_channels = 1; audio_file->setResamplingProperties(resample); fileProps = audio_file->getFileProperties(); // Refresh properties so that we have accurate information #ifndef NDEBUG // Retail has a few sounds that triggers this warning so we need to ignore those const char* warning_ignore_list[] = { "l_hit.wav", "m_hit.wav", "s_hit_2.wav", "Pirate.wav", }; bool show_warning = true; for (auto& name : warning_ignore_list) { if (!stricmp(name, entry->filename)) { show_warning = false; break; } } if (show_warning) { if (mod_supports_version(3, 8, 0)) { // This warning was introduced in 3.8.0 and caused a few issues since a lot of mods use 3D sounds // with more than one channel. This will silence the warnings for any mod that does not support // 3.8.0. Warning(LOCATION, "Sound '%s' has more than one channel but is used as a 3D sound! 3D sounds may only have " "one channel.", entry->filename); } else { mprintf(("Warning: Sound '%s' has more than one channel but is used as a 3D sound! 3D sounds may " "only have one channel.\n", entry->filename)); } } #endif } } // Load was a success si->n_channels = fileProps.num_channels; // 16-bit channel count (nChannels) si->sample_rate = fileProps.sample_rate; // 32-bit sample rate (nSamplesPerSec) si->avg_bytes_per_sec = fileProps.sample_rate * fileProps.bytes_per_sample * fileProps.num_channels; // 32-bit average bytes per second (nAvgBytesPerSec) si->bits = fileProps.bytes_per_sample * 8; // Read 16-bit bits per sample si->size = fileProps.total_samples * fileProps.bytes_per_sample * fileProps.num_channels; snd->uncompressed_size = si->size; auto rc = ds_load_buffer(&snd->sid, type, audio_file.get()); if (rc == -1) { nprintf(("Sound", "SOUND ==> Failed to load '%s'\n", entry->filename)); if (flags) *flags |= GAME_SND_NOT_VALID; return sound_load_id::invalid(); } // NOTE: "si" values can change once loaded in the buffer snd->duration = fl2i(1000.0f * fileProps.duration); strcpy_s( snd->filename, entry->filename ); snd->flags = SND_F_USED; snd->sig = snd_next_sig++; if (snd_next_sig < 0 ) snd_next_sig = 1; entry->id_sig = snd->sig; entry->id = sound_load_id(static_cast(n)); nprintf(("Sound", "SOUND ==> Finished loading '%s'\n", entry->filename)); return sound_load_id(static_cast(n)); } static SCP_set Unloaded_sound_ids; // --------------------------------------------------------------------------------------- // snd_unload() // // Unload a sound from memory. This will release the storage, and the sound must be re-loaded via // sound_load() before it can be played again. // int snd_unload(sound_load_id n) { if (!ds_initialized) return 0; if (!n.isValid()) { return 0; } if ((size_t)n.value() >= Sounds.size()) { return 0; } auto& snd = Sounds[n.value()]; ds_unload_buffer(snd.sid); Unloaded_sound_ids.emplace(n); if (snd.sid != -1) { Snd_sram -= snd.uncompressed_size; } //If this sound is at the end of the array, we might as well get rid of it if ((size_t)n.value() == Sounds.size() - 1) { Sounds.pop_back(); } else { snd.sid = -1; snd.flags &= ~SND_F_USED; } return 1; } // --------------------------------------------------------------------------------------- // snd_unload_all() // // Unload all sounds from memory. If there's a problem unloading a file the array may not be fully cleared // but future files will still use unused spots, so the array size shouldn't grow out of control. // void snd_unload_all() { while ( !Sounds.empty() ) { snd_unload(sound_load_id((int)(Sounds.size() - 1))); } snd_unload_cleanup(); } // If two tabled sounds share a sound file, unloading one will not correctly invalidate the index of the other, // causing a CTD when trying to play the other sound. // To fix this, every time we unload ANY sound, we must iterate over all tabled sounds, and invalidate their indices // if the given sound ID has been unloaded. void snd_unload_cleanup() { for (auto& snd : Snds) { for (auto& entry : snd.sound_entries) { if (Unloaded_sound_ids.contains(entry.id)) { entry.id = sound_load_id::invalid(); } } } for (auto& snd : Snds_iface) { for (auto& entry : snd.sound_entries) { if (Unloaded_sound_ids.contains(entry.id)) { entry.id = sound_load_id::invalid(); } } } Unloaded_sound_ids.clear(); } // --------------------------------------------------------------------------------------- // snd_close() // // This is the companion function to snd_init()... it closes down the game sound system. // void snd_close(void) { snd_stop_all(); if (!ds_initialized) return; snd_unload_all(); // free the sound data stored in DirectSound secondary buffers dscap_close(); // Close DirectSoundCapture ds_close(); // Close DirectSound off } // --------------------------------------------------------------------------------------- // snd_play_raw() // // Allow a sound to be played directly from the index in Sounds[]. This bypasses the // normal game sound management system. // // returns: -1 => sound could not be played // n => handle for instance of sound // sound_handle snd_play_raw(sound_load_id soundnum, float pan, float vol_scale, int priority, bool is_voice) { game_snd gs; if (!soundnum.isValid()) { return sound_handle::invalid(); } gs.sound_entries.resize(1); auto& entry = gs.sound_entries.back(); entry.id = soundnum; entry.id_sig = Sounds[soundnum.value()].sig; entry.filename[0] = 0; // entry.flags = GAME_SND_VOICE | GAME_SND_USE_DS3D; if (is_voice) { gs.flags = GAME_SND_VOICE; } gs.volume_range = util::UniformFloatRange(1.0f); gs.pitch_range = util::UniformFloatRange(1.0f); return snd_play(&gs, pan, vol_scale, priority, true); } MONITOR( NumSoundsStarted ) MONITOR( NumSoundsLoaded ) // --------------------------------------------------------------------------------------- // snd_play() // // NOTE: vol_scale parameter is the multiplicative scaling applied to the default volume // (vol_scale is a default parameter with a default value of 1.0f) // // input: gs => game-level sound description // pan => -1 (full left) to 1.0 (full right), this is a default parm // vol_scale => factor to scale default volume by (applied before global sound volume applied) // priority => SND_PRIORITY_MUST_PLAY // SND_PRIORITY_SINGLE_INSTANCE (default value) // SND_PRIORITY_DOUBLE_INSTANCE // SND_PRIORITY_TRIPLE_INSTANCE // // returns: -1 => sound could not be played // n => handle for instance of sound // sound_handle snd_play(game_snd* gs, float pan, float vol_scale, int priority, bool is_voice_msg) { float volume; loaded_sound* snd; if (!Sound_enabled) return sound_handle::invalid(); if (!ds_initialized) return sound_handle::invalid(); Assertion(gs != nullptr, "gamesnd parameter must not be null!"); if (gs == nullptr) { return sound_handle::invalid(); } if (gs->flags & GAME_SND_NOT_VALID) { return sound_handle::invalid(); } MONITOR_INC( NumSoundsStarted, 1 ); auto entry = gamesnd_choose_entry(gs); if (!entry->id.isValid()) { entry->id = snd_load(entry, &gs->flags); MONITOR_INC( NumSoundsLoaded, 1 ); } else if (entry->id_sig != Sounds[entry->id.value()].sig) { entry->id = snd_load(entry, &gs->flags); } if (!entry->id.isValid()) { Warning(LOCATION, "Failed to load one or more sounds for gamesnd %s!", gs->name.c_str()); gs->flags |= GAME_SND_NOT_VALID; return sound_handle::invalid(); } volume = gs->volume_range.next() * vol_scale; if ( gs->flags&GAME_SND_VOICE ) { volume *= (Master_voice_volume * aav_voice_volume); } else { volume *= (Master_sound_volume * aav_effect_volume); } if ( volume > 1.0f ) volume = 1.0f; snd = &Sounds[entry->id.value()]; if ( !(snd->flags & SND_F_USED) ) return sound_handle::invalid(); sound_handle handle; if ( volume > MIN_SOUND_VOLUME ) { handle = ds_play( snd->sid, entry->id_sig, ds_priority(priority), &gs->enhanced_sound_data, volume, pan, 0, is_voice_msg); if (handle.isValid()) { snd_set_pitch(handle, gs->pitch_range.next()); } } return handle; } MONITOR( Num3DSoundsStarted ) MONITOR( Num3DSoundsLoaded ) // --------------------------------------------------------------------------------------- // snd_play_3d() // // NOTE: vol_scale parameter is the multiplicative scaling applied to the default volume // (vol_scale is a default parameter with a default value of 1.0f) // // input: gs => game-level sound description // source_pos => global pos of where the sound is // listen_pos => global pos of where listener is // radius => optional parameter, this specifes distance at which to apply min/max distances // source_vel => velocity of the source playing the sound (used for DirectSound3D only) // looping => flag to indicate the sound should loop (default value 0) // vol_scale => factor to scale the static volume by (applied before attenuation) // priority => SND_PRIORITY_MUST_PLAY // SND_PRIORITY_SINGLE_INSTANCE (default value) // SND_PRIORITY_DOUBLE_INSTANCE // SND_PRIORITY_TRIPLE_INSTANCE // sound_fvec => forward vector of where sound is emitting from (RSX use only) // range_factor => factor N, which increases distance sound is heard by N times (default value 1) // // returns: -1 => sound could not be played // n => handle for instance of sound // sound_handle snd_play_3d(game_snd* gs, const vec3d* source_pos, const vec3d* listen_pos, float radius, const vec3d* source_vel, bool looping, float vol_scale, int priority, const vec3d* /*sound_fvec*/, float range_factor, bool force, bool /*is_ambient*/) { vec3d vector_to_sound; loaded_sound* snd; float volume, distance, max_volume; float min_range, max_range; float pan; if (!Sound_enabled) return sound_handle::invalid(); if (!ds_initialized) return sound_handle::invalid(); Assertion(gs != nullptr, "gamesnd parameter must not be null!"); if (gs == nullptr) { return sound_handle::invalid(); } if (gs->flags & GAME_SND_NOT_VALID) { return sound_handle::invalid(); } MONITOR_INC( Num3DSoundsStarted, 1 ); auto entry = gamesnd_choose_entry(gs); if (!entry->id.isValid()) { entry->id = snd_load(entry, &gs->flags); MONITOR_INC( Num3DSoundsLoaded, 1 ); } else if (entry->id_sig != Sounds[entry->id.value()].sig) { entry->id = snd_load(entry, &gs->flags); } if (!entry->id.isValid()) { Warning(LOCATION, "Failed to load one or more sounds for gamesnd %s!", gs->name.c_str()); gs->flags |= GAME_SND_NOT_VALID; return sound_handle::invalid(); } snd = &Sounds[entry->id.value()]; if ( !(snd->flags & SND_F_USED) ) return sound_handle::invalid(); if (snd->sid < 0) { return sound_handle::invalid(); } min_range = (gs->min + radius) * range_factor; max_range = (gs->max + radius) * range_factor; // DirectSound3D will not cut off sounds, no matter how quite they become.. so manually // prevent sounds from playing past the max distance. //IMPORTANT THIS IS NOT WORKING RIGHT OMG WTF distance = vm_vec_normalized_dir_quick( &vector_to_sound, source_pos, listen_pos ); if ( (distance > max_range) && !force){ return sound_handle::invalid(); } float default_volume = gs->volume_range.next(); max_volume = default_volume * vol_scale; if ( distance <= min_range ) { volume = max_volume; } else { volume = max_volume - max_volume*(distance/max_range); } if ( volume > 1.0f ){ volume = 1.0f; } if ( priority == SND_PRIORITY_MUST_PLAY ) { if ( volume < 0.3 ) { priority = SND_PRIORITY_DOUBLE_INSTANCE; } } volume *= (Master_sound_volume * aav_effect_volume); if ( (volume < MIN_SOUND_VOLUME) && !force) { return sound_handle::invalid(); } // any stereo sounds will not play in proper 3D, but they should have // been converted to mono already! Assertion( snd->info.n_channels == 1, "Sound should be mono! Sound file: %s", snd->filename ); sound_handle handle; if (Cmdline_no_3d_sound) { if (distance <= 0.0f) { pan = 0.0f; } else { pan = vm_vec_dot(&View_matrix.vec.rvec, &vector_to_sound); } handle = ds_play(snd->sid, entry->id_sig, ds_priority(priority), &gs->enhanced_sound_data, volume / default_volume, pan, looping); } else { handle = ds3d_play(snd->sid, entry->id_sig, source_pos, source_vel, min_range, max_range, looping, (max_volume*Master_sound_volume*aav_effect_volume), volume, &gs->enhanced_sound_data, ds_priority(priority)); } if (handle.isValid()) { if (looping) { currentlyLooping3dSoundInfos[handle] = { default_volume, vol_scale }; } snd_set_pitch(handle, gs->pitch_range.next()); } return handle; } // update the given 3d sound with a new position void snd_update_3d_pos(sound_handle soundnum, game_snd* gs, vec3d* new_pos, float radius, float range_factor) { if (Cmdline_no_3d_sound) { float vol, pan; // get new volume and pan vals snd_get_3d_vol_and_pan(gs, new_pos, &vol, &pan, radius, range_factor); // set volume snd_set_volume(soundnum, vol); // set pan snd_set_pan(soundnum, pan); } else { // MageKing17 - It's a 3D sound effect, we should use the function for setting the position of a 3D sound effect. int channel; if (!ds_initialized) return; Assertion( gs != NULL, "*gs was NULL in snd_update_3d_pos(); get a coder!\n" ); channel = ds_get_channel(soundnum); if (channel == -1) { nprintf(( "Sound", "WARNING: Trying to set position for a non-playing sound.\n" )); return; } float min_range = (float) (fl2i( (gs->min) * range_factor)); float max_range = (float) ((int)std::lround((gs->max) * range_factor)); ds3d_update_buffer(channel, min_range, max_range, new_pos, NULL); } } // --------------------------------------------------------------------------------------- // snd_get_3d_vol_and_pan() // // Based on the 3D position the player and the object, calculate // the correct volume and pan. // // parameters: gs => pointer to sound description // pos => 3D position used to calc volume and pan // vol => output parameter for the volume // pan => output parameter for the pan // radius => optional parameter (default value 0) which indicates sound attenuation // should occur from this radius // // returns: -1 => could not determine vol or pan // 0 => success // // NOTE: the volume is not scaled by the Master_sound_volume, since this always occurs // when snd_play() or snd_play_looping() is called // int snd_get_3d_vol_and_pan(game_snd *gs, vec3d *pos, float* vol, float *pan, float radius, float range_factor) { vec3d vector_to_sound; float distance, max_volume; *vol = 0.0f; *pan = 0.0f; if (!ds_initialized) return -1; Assertion( gs != NULL, "*gs was NULL in snd_get_3d_vol_and_pan(); get a coder!\n" ); float min_range = (float) (fl2i( (gs->min) * range_factor)); float max_range = (float) ((int)std::lround((gs->max) * range_factor)); distance = vm_vec_normalized_dir_quick( &vector_to_sound, pos, &View_position ); distance -= radius; max_volume = gs->volume_range.max(); if ( distance <= min_range ) { *vol = max_volume; } else { *vol = max_volume - (distance - min_range) * max_volume / (max_range - min_range); } if ( *vol > 1.0f ) *vol = 1.0f; if ( *vol > MIN_SOUND_VOLUME ) { if ( distance <= 0 ) *pan = 0.0f; else *pan = vm_vec_dot(&View_matrix.vec.rvec,&vector_to_sound); } return 0; } /** * Starts looping a game sound * * @param gs game-level sound description * @param pan -1.0 (full left) to 1.0 (full right) * @param start_loop TODO remove this parameter * @param stop_loop TODO remove this parameter * @param vol_scale factor to scale the static volume by (applied before attenuation) * @param scriptingUpdateVolume if true the looping sound value is updated default is TRUE * @return -1 on error, else the handle for this playing sound */ sound_handle snd_play_looping(game_snd* gs, float pan, int /*start_loop*/, int /*stop_loop*/, float vol_scale, int scriptingUpdateVolume) { float volume; loaded_sound* snd; if (!Sound_enabled) return sound_handle::invalid(); if (!ds_initialized) return sound_handle::invalid(); Assertion(gs != nullptr, "gamesnd parameter must not be null!"); if (gs == nullptr) { return sound_handle::invalid(); } if (gs->flags & GAME_SND_NOT_VALID) { return sound_handle::invalid(); } auto entry = gamesnd_choose_entry(gs); if (!entry->id.isValid()) { entry->id = snd_load(entry, &gs->flags); } else if (entry->id_sig != Sounds[entry->id.value()].sig) { entry->id = snd_load(entry, &gs->flags); } if (!entry->id.isValid()) { Warning(LOCATION, "Failed to load one or more sounds for gamesnd %s!", gs->name.c_str()); gs->flags |= GAME_SND_NOT_VALID; return sound_handle::invalid(); } snd = &Sounds[entry->id.value()]; if ( !(snd->flags & SND_F_USED) ) return sound_handle::invalid(); auto default_volume = gs->volume_range.next(); volume = default_volume * vol_scale; volume *= (Master_sound_volume * aav_effect_volume); if ( volume > 1.0f ) volume = 1.0f; sound_handle handle; if (volume > MIN_SOUND_VOLUME) { handle = ds_play( snd->sid, entry->id_sig, DS_MUST_PLAY, &gs->enhanced_sound_data, volume, pan, 1); if (handle.isValid()) { if (scriptingUpdateVolume) { currentlyLoopingSoundInfos[handle] = { default_volume, vol_scale }; } snd_set_pitch(handle, gs->pitch_range.next()); } } return handle; } /** * Stop a sound from playing. * * @param sig handle to sound, what is returned from snd_play() */ void snd_stop(sound_handle sig) { int channel; if (!ds_initialized) return; if (!sig.isValid()) return; channel = ds_get_channel(sig); if ( channel == -1 ) return; currentlyLoopingSoundInfos.erase(sig); currentlyLooping3dSoundInfos.erase(sig); ds_stop_channel(channel); } /** * Pauses a sound * * @param sig handle to sound, what is returned from snd_play() */ void snd_pause(sound_handle sig) { if (!ds_initialized) return; if (!sig.isValid()) return; int channel = ds_get_channel(sig); if (channel == -1) return; ds_pause_channel(channel); } /** * Resumes a sound * * @param sig handle to sound, what is returned from snd_play() */ void snd_resume(sound_handle sig) { if (!ds_initialized) return; if (!sig.isValid()) return; int channel = ds_get_channel_raw(sig); if (channel == -1) return; ds_resume_channel(channel); } /** * Stop all playing sound channels (including looping sounds) * * NOTE: This stops all sounds that are playing from Channels[] sound buffers. * It doesn't stop every secondary sound buffer in existance. */ void snd_stop_all() { if (!ds_initialized) return; currentlyLoopingSoundInfos.clear(); currentlyLooping3dSoundInfos.clear(); ds_stop_channel_all(); } /** * Set the volume of a currently playing sound * * @param sig handle to sound, what is returned from snd_play() * @param volume volume of sound (range: 0.0 -> 1.0) */ void snd_set_volume(sound_handle sig, float volume, bool is_voice) { int channel; float new_volume; if (!ds_initialized) return; if (!sig.isValid()) return; channel = ds_get_channel(sig); if ( channel == -1 ) { nprintf(( "Sound", "WARNING: Trying to set volume for a non-playing sound.\n" )); return; } auto iter = currentlyLoopingSoundInfos.find(sig); if (iter != currentlyLoopingSoundInfos.end()) { iter->second.m_dynamicVolume = volume; } else { iter = currentlyLooping3dSoundInfos.find(sig); if (iter != currentlyLooping3dSoundInfos.end()) { iter->second.m_dynamicVolume = volume; } } if (is_voice) { new_volume = volume * (Master_voice_volume * aav_voice_volume); } else { new_volume = volume * (Master_sound_volume * aav_effect_volume); } ds_set_volume( channel, new_volume ); } // --------------------------------------------------------------------------------------- // snd_set_pan() // // Set the pan of a currently playing sound // // parameters: sig => handle to sound, what is returned from snd_play() // pan => pan of sound (range: -1.0 -> 1.0) // void snd_set_pan(sound_handle sig, float pan) { int channel; if (!ds_initialized) return; if (!sig.isValid()) return; channel = ds_get_channel(sig); if ( channel == -1 ) { nprintf(( "Sound", "WARNING: Trying to set pan for a non-playing sound.\n" )); return; } ds_set_pan( channel, pan ); } // --------------------------------------------------------------------------------------- // snd_get_pitch() // // Return the pitch of a currently playing sound // // returns: pitch of sound ( range: 100 to 100000) // // parameters: sig => handle to sound, what is returned from snd_play() // float snd_get_pitch(sound_handle sig) { int channel; if (!ds_initialized) return -1; if (!sig.isValid()) return -1; channel = ds_get_channel(sig); if ( channel == -1 ) { nprintf(( "Sound", "WARNING: Trying to get pitch for a non-playing sound.\n" )); return -1; } return ds_get_pitch(channel); } // --------------------------------------------------------------------------------------- // snd_set_pitch() // // Set the pitch of a currently playing sound // // parameters: sig => handle to sound, what is returned from snd_play() // pan => pitch of sound (must be greater than zero) // void snd_set_pitch(sound_handle sig, float pitch) { int channel; if (!ds_initialized) return; if (!sig.isValid()) return; channel = ds_get_channel(sig); if ( channel == -1 ) { nprintf(( "Sound", "WARNING: Trying to set pitch for a non-playing sound.\n" )); return; } ds_set_pitch(channel, pitch); } // --------------------------------------------------------------------------------------- // snd_is_playing() // // Determine if a sound is playing // // returns: 1 => sound is currently playing // 0 => sound is not playing // // parameters: sig => signature of sound, what is returned from snd_play() // int snd_is_playing(sound_handle sig) { int channel, is_playing; if (!ds_initialized) return 0; if (!sig.isValid()) return 0; channel = ds_get_channel(sig); if ( channel == -1 ) return 0; is_playing = ds_is_channel_playing(channel); if ( is_playing == TRUE ) { return 1; } return 0; } // --------------------------------------------------------------------------------------- // snd_is_paused() // // Determine if a sound is paused // // returns: 1 => sound is currently paused // 0 => sound is not paused // // parameters: sig => signature of sound, what is returned from snd_play() // bool snd_is_paused(sound_handle sig) { if (!ds_initialized) return false; if (!sig.isValid()) return false; int channel = ds_get_channel_raw(sig); if (channel == -1) return false; return ds_is_channel_paused(channel); } // --------------------------------------------------------------------------------------- // snd_is_inited() // // int snd_is_inited() { if ( !ds_initialized ) return FALSE; return TRUE; } // return the time in ms for the duration of the sound int snd_get_duration(sound_load_id snd_id) { if (!snd_id.isValid()) return 0; Assertion( !Sounds.empty(), "Sounds vector is empty. Why are we trying to look up an index?\n" ); if ( Sounds.empty() ) return 0; Assertion(Sounds[snd_id.value()].duration > 0, "Sound duration for sound %s is bogus (%d)\n", Sounds[snd_id.value()].filename, Sounds[snd_id.value()].duration); if (Sounds[snd_id.value()].duration > 0) return Sounds[snd_id.value()].duration; else return 0; } // return the time in ms for the duration of the sound const char* snd_get_filename(sound_load_id snd_id) { Assertion(snd_id.isValid(), "Invalid sound id %d!", snd_id.value()); return Sounds[snd_id.value()].filename; } MONITOR( SoundChannels ) // update the position of the listener for the specific 3D sound API we're // using void snd_update_listener(vec3d *pos, vec3d *vel, matrix *orient) { MONITOR_INC( SoundChannels, ds_get_number_channels() ); ds3d_update_listener(pos, vel, orient); } // this could probably be optimized a bit void snd_rewind(sound_handle snd_handle, float seconds) { float current_time,desired_time; float bps; uint32_t current_offset,desired_offset; sound_info *snd; if(!snd_is_playing(snd_handle)) return; auto channel = ds_get_channel(snd_handle); snd = &Sounds[snd_get_sound_id(snd_handle).value()].info; current_offset = ds_get_play_position(channel); // current offset into the sound bps = (float)snd->sample_rate * (float)snd->bits; // data rate current_time = (float)current_offset/bps; // how many seconds we're into the sound // don't rewind if it'll put us before the beginning of the sound if(current_time - seconds < 0.0f) return; desired_time = current_time - seconds; // where we want to be desired_offset = (uint32_t)(desired_time * bps); // the target ds_set_position(channel, desired_offset); } // this could probably be optimized a bit void snd_ffwd(sound_handle snd_handle, float seconds) { float current_time,desired_time; float bps; uint32_t current_offset,desired_offset; sound_info *snd; if(!snd_is_playing(snd_handle)) return; auto channel = ds_get_channel(snd_handle); snd = &Sounds[snd_get_sound_id(snd_handle).value()].info; current_offset = ds_get_play_position(channel); // current offset into the sound bps = (float)snd->sample_rate * (float)snd->bits; // data rate current_time = (float)current_offset/bps; // how many seconds we're into the sound // don't rewind if it'll put us past the end of the sound if(current_time + seconds > (float)snd->duration) return; desired_time = current_time + seconds; // where we want to be desired_offset = (uint32_t)(desired_time * bps); // the target ds_set_position(channel,desired_offset); } // this could probably be optimized a bit void snd_set_pos(sound_handle snd_handle, float val, int as_pct) { if(!snd_is_playing(snd_handle)) return; auto id_handle = snd_get_sound_id(snd_handle); Assertion(SCP_vector_inbounds(Sounds, id_handle.value()), "Attempt to get a sound %i that is not loaded, please report!", id_handle.value()); const auto& snd = Sounds[id_handle.value()].info; // set position as an absolute from 0 to 1 if(as_pct){ Assert((val >= 0.0) && (val <= 1.0)); ds_set_position(ds_get_channel(snd_handle), static_cast((static_cast(snd.size) * val))); } // set the position as an absolute # of seconds from the beginning of the sound else { float bps; Assert(val <= static_cast(snd.duration) / 1000.0f); bps = static_cast(snd.sample_rate) * static_cast(snd.bits); // data rate ds_set_position(ds_get_channel(snd_handle), static_cast(bps * val)); } } // Return the number of sounds currently playing int snd_num_playing() { return ds_get_number_channels(); } // Return the raw sound data for a loaded sound // // input: handle => index into Sounds[] array // data => allocated mem to hold sound // // exit: 0 => success // !0 => fail int snd_get_data(sound_load_id handle, char* data) { Assert(handle.isValid()); if (ds_get_data(Sounds[handle.value()].sid, data)) { return -1; } return 0; } // return the size of the sound data associated with the sound handle int snd_size(sound_load_id handle, int* size) { Assert(handle.isValid()); if (ds_get_size(Sounds[handle.value()].sid, size)) { return -1; } return 0; } // retrieve the bits per sample and frequency for a given sound void snd_get_format(sound_load_id handle, int* bits_per_sample, int* frequency) { Assert((handle.isValid()) && ((size_t)handle.value() < Sounds.size())); if (bits_per_sample) *bits_per_sample = Sounds[handle.value()].info.bits; if (frequency) *frequency = Sounds[handle.value()].info.sample_rate; } // return the time for the sound to play in milliseconds int snd_time_remaining(sound_handle handle) { int channel, is_playing, time_remaining = 0; if (!ds_initialized) return 0; if (!handle.isValid()) return 0; channel = ds_get_channel(handle); if ( channel == -1 ) return 0; is_playing = ds_is_channel_playing(channel); if ( !is_playing ) { return 0; } int current_offset, max_offset; int bits_per_sample = 0, frequency = 0; auto sdx = snd_get_sound_id(handle); if (!sdx.isValid()) { Int3(); return 0; } snd_get_format(sdx, &bits_per_sample, &frequency); if ( (bits_per_sample <= 0) || (frequency <= 0) ) return 0; // handle ADPCM properly. It's always 16bit for OpenAL but should be 8 or 16 // for Windows. We can't leave it as 4 here (the ADPCM rate) because that is the // compressed bps and the math is against the uncompressed bps. if ( bits_per_sample == 4 ) { bits_per_sample = 16; } Assert( bits_per_sample >= 8 ); current_offset = ds_get_play_position(channel); max_offset = ds_get_channel_size(channel); if ( current_offset < max_offset ) { int bytes_remaining = max_offset - current_offset; int samples_remaining = bytes_remaining / (bits_per_sample/8); time_remaining = (int)std::lround(1000.0f * samples_remaining/frequency); } // mprintf(("time_remaining: %d\n", time_remaining)); return time_remaining; } sound_load_id snd_get_sound_id(sound_handle snd_handle) { if (!ds_initialized) { return sound_load_id::invalid(); } auto channel = ds_get_channel(snd_handle); if (channel < 0) { return sound_load_id::invalid(); } auto channel_id = ds_get_sound_id(channel); for (size_t i = 0; i < Sounds.size(); i++) { if ((Sounds[i].flags & SND_F_USED) && (Sounds[i].sig == channel_id)) { return sound_load_id((int)i); } } return sound_load_id::invalid(); } // snd_env_ interface static int Sound_env_id; static float Sound_env_volume; static float Sound_env_damping; static float Sound_env_decay; // Set the sound environment // int sound_env_set(sound_env *se) { if (ds_eax_set_all(se->id, se->volume, se->damping, se->decay) == 0) { Sound_env_id = se->id; Sound_env_volume = se->volume; Sound_env_damping = se->damping; Sound_env_decay = se->decay; return 0; } else { return -1; } } // Get the sound environment // int sound_env_get(sound_env *se, int preset) { EAX_REVERBPROPERTIES er; if (ds_eax_get_all(&er, preset) == 0) { se->id = (int)er.environment; se->volume = er.fVolume; se->decay = er.fDecayTime_sec; se->damping = er.fDamping; return 0; } else { return -1; } } // Turn off the sound environment // int sound_env_disable() { sound_env se; se.id = EAX_ENVIRONMENT_GENERIC; se.volume = 0.0f; se.damping = 0.0f; se.decay = 0.0f; sound_env_set(&se); return 0; } // Return 1 if EAX can used to set the sound environment, otherwise return 0 // int sound_env_supported() { return ds_eax_is_inited(); } void adjust_volume_on_frame(float* volume_now, aav* data); // Called once per game frame // void snd_do_frame() { adjust_volume_on_frame(&aav_music_volume, &aav_data[AAV_MUSIC]); adjust_volume_on_frame(&aav_voice_volume, &aav_data[AAV_VOICE]); adjust_volume_on_frame(&aav_effect_volume, &aav_data[AAV_EFFECTS]); ds_do_frame(); } void update_looping_sound_volumes(SCP_unordered_map &looping_sounds) { for (const auto &looping_sound_pair : looping_sounds) { const auto& dsHandle = looping_sound_pair.first; const auto& looping_sound = looping_sound_pair.second; const float new_volume = looping_sound.m_defaultVolume * looping_sound.m_dynamicVolume * (Master_sound_volume * aav_effect_volume); ds_set_volume(ds_get_channel(dsHandle), new_volume); } } void snd_adjust_audio_volume(int aav_type, float percent, int delta_time) { Assertion(aav_type >= 0 && aav_type <= 2, "aav_type is out of range!"); if (aav_type < 0 || aav_type > 2) return; aav_data[aav_type].start_volume = *aav_volume[aav_type]; if (percent < *aav_volume[aav_type]) aav_data[aav_type].delta = (*aav_volume[aav_type] - percent) * -1.0f; else aav_data[aav_type].delta = percent - *aav_volume[aav_type]; aav_data[aav_type].delta_time = delta_time; aav_data[aav_type].start_time = f2fl(Missiontime) * MILLISECONDS_PER_SECOND; // if the delta_time is 0 [immediate], make the relevant updates without waiting for the next frame's snd_do_frame() if (delta_time <= 0) adjust_volume_on_frame(aav_volume[aav_type], &aav_data[aav_type]); } void adjust_volume_on_frame(float* volume_now, aav* data) { if (Missiontime == 0){ return; } if (*volume_now == (data->start_volume + data->delta)) return; float msMissiontime = f2fl(Missiontime) * MILLISECONDS_PER_SECOND; //How much change do we need? //if immediate, or if the time has elapsed, use the target volume if (data->delta_time <= 0 || msMissiontime >= (data->start_time + data->delta_time)) *volume_now = data->start_volume + data->delta; else { float done = (msMissiontime - data->start_time) / data->delta_time; //apply change *volume_now = data->start_volume + (data->delta * done); CLAMP(*volume_now, 0.0f, 1.0f); } // if setting music volume, trigger volume change in playing tracks // done here in order to avoid setting music volume in every frame regardless if it changed or not if (&aav_music_volume == volume_now) { audiostream_set_volume_all(Master_event_music_volume * aav_music_volume, ASF_EVENTMUSIC); } // for similar reasons, if setting effects volume, trigger volume change here else if (&aav_effect_volume == volume_now) { update_looping_sound_volumes(currentlyLoopingSoundInfos); update_looping_sound_volumes(currentlyLooping3dSoundInfos); } } void snd_aav_init() { aav_music_volume = 1.0f; aav_voice_volume = 1.0f; aav_effect_volume = 1.0f; for (int i = 0; i < 3; i++) { aav_data[i].delta = 0.0f; aav_data[i].start_volume = 1.0f; aav_data[i].delta_time = 0; aav_data[i].start_time = 0.0f; } } void snd_set_effects_volume(float volume) { Assertion(volume >= 0.0f && volume <= 1.0f, "Invalid effects volume %f!", volume); Master_sound_volume = volume; main_hall_reset_ambient_vol(); } void snd_set_voice_volume(float volume) { Assertion(volume >= 0.0f && volume <= 1.0f, "Invalid voice volume %f!", volume); Master_voice_volume = volume; audiostream_set_volume_all(Master_voice_volume, ASF_VOICE); } game_snd::game_snd() : last_entry_index(std::numeric_limits::max()) { } EnhancedSoundData::EnhancedSoundData() { } EnhancedSoundData::EnhancedSoundData(const int new_priority, const unsigned int new_limit) : priority(new_priority), limit(new_limit) { Assertion(priority >= SND_ENHANCED_PRIORITY_MUST_PLAY && priority <= SND_ENHANCED_PRIORITY_LOW, "EnhancedSoundData ctor given invalid priority %d", priority); Assertion(limit > 0, "EnhancedSoundData ctor given invalid limit %d", limit); } game_snd_entry::game_snd_entry() { filename[0] = 0; }