Files
fs2open.github.com/code/sound/sound.cpp
T

1702 lines
46 KiB
C++

/*
* 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 <climits>
#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<loaded_sound> 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<float>("Audio.Effects",
std::pair<const char*, int>{"Effects", 1370},
std::pair<const char*, int>{"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<float>("Audio.Voice",
std::pair<const char*, int>{"Voice", 1372},
std::pair<const char*, int>{"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<sound_handle, LoopingSoundInfo, util::ID_Hash> currentlyLoopingSoundInfos;
SCP_unordered_map<sound_handle, LoopingSoundInfo, util::ID_Hash> 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<game_snd> Snds;
extern SCP_vector<game_snd> 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<game_snd>::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<game_snd>::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<sound::IAudioFile> openAudioFile(const char* fileName)
{
#ifdef WITH_FFMPEG
{
std::unique_ptr<sound::IAudioFile> 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<int>(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<sound::IAudioFile> 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<int>(n));
nprintf(("Sound", "SOUND ==> Finished loading '%s'\n", entry->filename));
return sound_load_id(static_cast<int>(n));
}
static SCP_set<sound_load_id> 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();
if (gs == nullptr) {
UNREACHABLE("gamesnd parameter must not be null!");
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();
if (gs == nullptr) {
UNREACHABLE("gamesnd parameter must not be null!");
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();
if (gs == nullptr) {
UNREACHABLE("gamesnd parameter must not be null!");
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<uint32_t>((static_cast<float>(snd.size) * val)));
}
// set the position as an absolute # of seconds from the beginning of the sound
else {
float bps;
Assert(val <= static_cast<float>(snd.duration) / 1000.0f);
bps = static_cast<float>(snd.sample_rate) * static_cast<float>(snd.bits); // data rate
ds_set_position(ds_get_channel(snd_handle), static_cast<uint32_t>(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<sound_handle, LoopingSoundInfo, util::ID_Hash> &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<size_t>::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;
}