/* * Copyright (C) 2005-2008, 2011 by the Widelands Development Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef WL_SOUND_SOUND_HANDLER_H #define WL_SOUND_SOUND_HANDLER_H #include #include #include #include #include #ifndef _MSC_VER #include #endif #include "logic/widelands_geometry.h" #include "random/random.h" #include "sound/fxset.h" namespace Widelands {class EditorGameBase;} struct Songset; struct SDL_mutex; class FileRead; /// How many milliseconds in the past to consider for /// SoundHandler::play_or_not() #define SLIDING_WINDOW_SIZE 20000 extern class SoundHandler g_sound_handler; /** The 'sound server' for Widelands. * * SoundHandler collects all functions for dealing with music and sound effects * in one class. It is similar in task - though not in scope - to well known * sound servers like gstreamer, EsounD or aRts. For the moment (and probably * forever), the only backend supported is SDL_mixer. * * \par Music * * Background music for different situations (e.g. 'Menu', 'Gameplay') is * collected in songsets. Each Songset contains references to one or more * songs in ogg format. The only ordering inside a soundset is from the order * in which the songs were loaded. * * Other classes can request to start or stop playing a certain songset, * changing the songset is provided as a convenience method. It is also * possible to switch to some other piece inside the same songset - but there * is \e no control over \e which song out of a songset gets played. The * selection is either linear (the order in which the songs were loaded) or * completely random. * * The files for the predefined system songsets * \li \c intro * \li \c menu * \li \c ingame * * must reside directly in the directory 'sounds' and must be named * SONGSET_XX.??? where XX is a number from 00 to 99 and ??? is a filename * extension. All subdirectories of 'sounds' will be considered to contain * ingame music. The the music and sub-subdirectories found in them can be * arbitrarily named. This means: everything below sound/ingame_01 can have * any name you want. All audio files below sound/ingame_01 will be played as * ingame music. * * For more information about the naming scheme, see load_fx() * * You should be using the ogg format for music. * * \par Sound effects * * Buildings and workers can use sound effects in their programs. To do so, use * e.g. "play_sound blacksmith_hammer" in the appropriate conf file. The conf file * parser will then load one or more audio files for 'hammering blacksmith' * from the building's/worker's configuration directory and store them in an * FXset for later access, similar to the way music is stored in songsets. * For effects, however, the selection is always random. Sound effects are kept * in memory at all times, to avoid delays from disk access. * * The abovementioned sound effects are synchronized with a work program. It's * also possible to have sound effects that are synchronized with a * building/worker \e animation. For more information about this look at class * AnimationManager. * * \par Usage of callbacks * * SDL_mixer's way to notify the application of important sound events, e.g. * that a song is finished, are callbacks. While callbacks in and of themselves * are a fine thing, they can also be a pain in the body part with which we * usually touch our chairs. * * Problem 1: * * Callbacks must use global(or static) functions \e but \e not normal member * functions of a class. If you must know why: ask google. But how can a * static function share data with an instance of it's own class? Usually not at * all. * * Fortunately, g_sound_handler already is a global variable, * and therefore accessible to global functions. So problem 1 disappears. * * Problem 2: * * Callbacks run in the caller's context. This means that when * music_finished_callback() is called, SDL_mixer and SDL_audio will * be holding all of their locks!! "So what?", you ask. The above means * that one \e must \e not call \b any SDL_mixer functions from inside the * callback, otherwise a deadlock is guaranteed. This indirectly does include * start_music(), stop_music() and of course change_music(). * Unfortunately, that's just the functions we'd need to execute from the * callback. As if that was not enough, SDL_mixer internally uses * two separate threads, so you \e really don't want to play around with * locking. * * The only way around that resctriction is to send an SDL event(SDL_USEREVENT) * from the callback (non-sound SDL functions \e can be used). Then, later, * the main event loop will process this event \e but \e not in * SDL_mixer's context, so locking is no problem. * * Yes, that's just a tad ugly. * * No, there's no other way. At least none that I found. * * \par Stopping music without blocking * * When playing background music with SDL_mixer, we can fade the audio in/out. * Unfortunately, Mix_FadeOutMusic() will return immediately - but, as the music * is not yet stopped, starting a new piece of background music will block. So * the choice is to block (directly) after ordering to fade out or indirectly * when starting the next piece. Now imagine a fadeout-time of 30 seconds ... * and the user who is waiting for the next screen ...... * * The solution is to work asynchronously, which is doable, as we already use a * callback to tell us when the audio is \e completely finished. So in * stop_music() (or change_music()) we just start the fadeout. The * callback then tells us when the audio has actually stopped and we can start * the next music. To differentiate between the two states we can just take a * peek with Mix_MusicPlaying() if there is music running. To make sure that * nothing bad happens, that check is not only required in change_music() * but also in start_music(), which causes the seemingly recursive call to * change_music() from inside start_music(). It really is not recursive, trust * me :-) */ // TODO(unknown): DOC: priorities // TODO(unknown): DOC: play-or-not algorithm // TODO(unknown): Environmental sound effects (e.g. wind) // TODO(unknown): repair and reenable animation sound effects for 1-pic-animations // TODO(unknown): accommodate runtime changes of i18n language // TODO(unknown): accommodate sound activation if it was disabled at the beginning // This is used for SDL UserEvents to be handled in the main loop. enum { CHANGE_MUSIC }; class SoundHandler { friend struct Songset; friend struct FXset; public: SoundHandler(); ~SoundHandler(); void init(); void shutdown(); void read_config(); void load_system_sounds(); void load_fx_if_needed (const std::string & dir, const std::string & basename, const std::string & fx_name); void play_fx (const std::string & fx_name, Widelands::Coords map_position, uint8_t priority = PRIO_ALLOW_MULTIPLE + PRIO_MEDIUM); void play_fx (const std::string & fx_name, int32_t stereo_position, uint8_t priority = PRIO_ALLOW_MULTIPLE + PRIO_MEDIUM); void register_song (const std::string & dir, const std::string & basename); void start_music(const std::string & songset_name, int32_t fadein_ms = 0); void stop_music(int32_t fadeout_ms = 0); void change_music (const std::string & songset_name = std::string(), int32_t fadeout_ms = 0, int32_t fadein_ms = 0); static void music_finished_callback(); static void fx_finished_callback(int32_t channel); void handle_channel_finished(uint32_t channel); bool get_disable_music() const; bool get_disable_fx () const; int32_t get_music_volume () const; int32_t get_fx_volume () const; void set_disable_music(bool disable); void set_disable_fx (bool disable); void set_music_volume (int32_t volume); void set_fx_volume (int32_t volume); /** * Return the max value for volume settings. We use a function to hide * SDL_mixer constants outside of sound_handler. */ int32_t get_max_volume() const {return MIX_MAX_VOLUME;} /** The game logic where we can get a mapping from logical to screen * coordinates and vice vers */ Widelands::EditorGameBase * egbase_; /** Only for buffering the command line option --nosound until real initialization is done. * \see SoundHandler::SoundHandler() * \see SoundHandler::init() */ // TODO(unknown): This is ugly. Find a better way to do it bool nosound_; /** Can disable_music_ and disable_fx_ be changed? * true = they mustn't be changed (e.g. because hardware is missing) * false = can be changed at user request */ bool lock_audio_disabling_; protected: // Prints an error and disables the sound system. void initialization_error(const std::string& msg); void load_one_fx(const std::string& path, const std::string& fx_name); int32_t stereo_position(Widelands::Coords position); bool play_or_not (const std::string & fx_name, int32_t stereo_position, uint8_t priority); /// Whether to disable background music bool disable_music_; /// Whether to disable sound effects bool disable_fx_; /// Volume of music (from 0 to get_max_volume()) int32_t music_volume_; /// Volume of sound effects (from 0 to get_max_volume()) int32_t fx_volume_; /** Whether to play music in random order * \note Sound effects will \e always be selected at random (inside * their FXset, of course. */ bool random_order_; /// A collection of songsets using SongsetMap = std::map>; SongsetMap songs_; /// A collection of effect sets using FXsetMap = std::map>; FXsetMap fxs_; /// List of currently playing effects, and the channel each one is on /// Access to this variable is protected through fx_lock_ mutex. using ActivefxMap = std::map; ActivefxMap active_fx_; /** Which songset we are currently selecting songs from - not regarding * if there actually is a song playing \e right \e now. */ std::string current_songset_; /** The random number generator. * \note The RNG here \e must \e not be the same as the one for the game * logic! */ RNG rng_; /// Protects access to active_fx_ between callbacks and main code. SDL_mutex * fx_lock_; }; #endif // end of include guard: WL_SOUND_SOUND_HANDLER_H