~widelands-dev/widelands/bug-1559729-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/*
 * 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 <cstring>
#include <map>
#include <memory>
#include <string>
#include <vector>

#ifndef _MSC_VER
#include <unistd.h>
#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 <b>will
 * be holding all of their locks!!</b> "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<std::string, std::unique_ptr<Songset>>;
	SongsetMap songs_;

	/// A collection of effect sets
	using FXsetMap = std::map<std::string, std::unique_ptr<FXset>>;
	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<uint32_t, std::string>;
	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