1 #if defined(SDL_SOUND)
2 
3 #include "sdlsound.h"
4 
5 #include <cstdlib>
6 #include <algorithm>
7 #include <chrono>
8 #include <map>
9 #include <string>
10 #include <unordered_map>
11 #include <vector>
12 #include <exception>
13 #include <memory>
14 #include <ostream>
15 #include <utility>
16 
17 #if defined(_MSC_VER) && defined(USE_VCPKG)
18 #    include <SDL2/SDL_mixer.h>
19 #else
20 #    include <SDL_mixer.h>
21 #endif
22 
23 #include "cached_options.h"
24 #include "debug.h"
25 #include "init.h"
26 #include "json.h"
27 #include "loading_ui.h"
28 #include "messages.h"
29 #include "options.h"
30 #include "path_info.h"
31 #include "rng.h"
32 #include "sdl_wrappers.h"
33 #include "sounds.h"
34 #include "units.h"
35 
36 #define dbg(x) DebugLog((x),D_SDL) << __FILE__ << ":" << __LINE__ << ": "
37 
38 using id_and_variant = std::pair<std::string, std::string>;
39 struct sound_effect_resource {
40     std::string path;
41     struct deleter {
42         // Operator overloaded to leverage deletion API.
operator ()sound_effect_resource::deleter43         void operator()( Mix_Chunk *const c ) const {
44             Mix_FreeChunk( c );
45         }
46     };
47     std::unique_ptr<Mix_Chunk, deleter> chunk;
48 };
49 struct sound_effect {
50     int volume = 0;
51     int resource_id = 0;
52 };
53 struct sfx_resources_t {
54     std::vector<sound_effect_resource> resource;
55     std::map<id_and_variant, std::vector<sound_effect>> sound_effects;
56 };
57 struct music_playlist {
58     // list of filenames relative to the soundpack location
59     struct entry {
60         std::string file;
61         int volume;
62     };
63     std::vector<entry> entries;
64     bool shuffle;
65 
music_playlistmusic_playlist66     music_playlist() : shuffle( false ) {
67     }
68 };
69 /** The music we're currently playing. */
70 static Mix_Music *current_music = nullptr;
71 static int current_music_track_volume = 0;
72 static std::string current_playlist;
73 static size_t current_playlist_at = 0;
74 static size_t absolute_playlist_at = 0;
75 static std::vector<std::size_t> playlist_indexes;
76 static bool sound_init_success = false;
77 static std::map<std::string, music_playlist> playlists;
78 static std::string current_soundpack_path;
79 
80 static std::unordered_map<std::string, int> unique_paths;
81 static sfx_resources_t sfx_resources;
82 static std::vector<id_and_variant> sfx_preload;
83 
84 bool sounds::sound_enabled = false;
85 
check_sound(const int volume=1)86 static inline bool check_sound( const int volume = 1 )
87 {
88     return( sound_init_success && sounds::sound_enabled && volume > 0 );
89 }
90 
91 /**
92  * Attempt to initialize an audio device.  Returns false if initialization fails.
93  */
init_sound()94 bool init_sound()
95 {
96     int audio_rate = 44100;
97     Uint16 audio_format = AUDIO_S16;
98     int audio_channels = 2;
99     int audio_buffers = 2048;
100 
101     // We should only need to init once
102     if( !sound_init_success ) {
103         // Mix_OpenAudio returns non-zero if something went wrong trying to open the device
104         if( !Mix_OpenAudio( audio_rate, audio_format, audio_channels, audio_buffers ) ) {
105             Mix_AllocateChannels( 128 );
106             Mix_ReserveChannels( static_cast<int>( sfx::channel::MAX_CHANNEL ) );
107 
108             // For the sound effects system.
109             Mix_GroupChannels( static_cast<int>( sfx::channel::daytime_outdoors_env ),
110                                static_cast<int>( sfx::channel::nighttime_outdoors_env ),
111                                static_cast<int>( sfx::group::time_of_day ) );
112             Mix_GroupChannels( static_cast<int>( sfx::channel::underground_env ),
113                                static_cast<int>( sfx::channel::outdoor_blizzard ),
114                                static_cast<int>( sfx::group::weather ) );
115             Mix_GroupChannels( static_cast<int>( sfx::channel::danger_extreme_theme ),
116                                static_cast<int>( sfx::channel::danger_low_theme ),
117                                static_cast<int>( sfx::group::context_themes ) );
118             Mix_GroupChannels( static_cast<int>( sfx::channel::stamina_75 ),
119                                static_cast<int>( sfx::channel::stamina_35 ),
120                                static_cast<int>( sfx::group::fatigue ) );
121 
122             sound_init_success = true;
123         } else {
124             dbg( D_ERROR ) << "Failed to open audio mixer, sound won't work: " << Mix_GetError();
125         }
126     }
127 
128     return sound_init_success;
129 }
shutdown_sound()130 void shutdown_sound()
131 {
132     // De-allocate all loaded sound.
133     sfx_resources.resource.clear();
134     sfx_resources.sound_effects.clear();
135 
136     playlists.clear();
137     Mix_CloseAudio();
138 }
139 
140 static void musicFinished();
141 
play_music_file(const std::string & filename,int volume)142 static void play_music_file( const std::string &filename, int volume )
143 {
144     if( test_mode ) {
145         return;
146     }
147 
148     if( !check_sound( volume ) ) {
149         return;
150     }
151 
152     const std::string path = ( current_soundpack_path + "/" + filename );
153     current_music = Mix_LoadMUS( path.c_str() );
154     if( current_music == nullptr ) {
155         dbg( D_ERROR ) << "Failed to load audio file " << path << ": " << Mix_GetError();
156         return;
157     }
158     Mix_VolumeMusic( volume * get_option<int>( "MUSIC_VOLUME" ) / 100 );
159     if( Mix_PlayMusic( current_music, 0 ) != 0 ) {
160         dbg( D_ERROR ) << "Starting playlist " << path << " failed: " << Mix_GetError();
161         return;
162     }
163     Mix_HookMusicFinished( musicFinished );
164 }
165 
166 /** Callback called when we finish playing music. */
musicFinished()167 void musicFinished()
168 {
169     if( test_mode ) {
170         return;
171     }
172 
173     Mix_HaltMusic();
174     Mix_FreeMusic( current_music );
175     current_music = nullptr;
176 
177     const auto iter = playlists.find( current_playlist );
178     if( iter == playlists.end() ) {
179         return;
180     }
181     const music_playlist &list = iter->second;
182     if( list.entries.empty() ) {
183         return;
184     }
185 
186     // Load the next file to play.
187     absolute_playlist_at++;
188 
189     // Wrap around if we reached the end of the playlist.
190     if( absolute_playlist_at >= list.entries.size() ) {
191         absolute_playlist_at = 0;
192     }
193 
194     current_playlist_at = playlist_indexes.at( absolute_playlist_at );
195 
196     const music_playlist::entry &next = list.entries[current_playlist_at];
197     play_music_file( next.file, next.volume );
198 }
199 
play_music(const std::string & playlist)200 void play_music( const std::string &playlist )
201 {
202     const auto iter = playlists.find( playlist );
203     if( iter == playlists.end() ) {
204         return;
205     }
206     const music_playlist &list = iter->second;
207     if( list.entries.empty() ) {
208         return;
209     }
210 
211     // Don't interrupt playlist that's already playing.
212     if( playlist == current_playlist ) {
213         return;
214     }
215 
216     for( size_t i = 0; i < list.entries.size(); i++ ) {
217         playlist_indexes.push_back( i );
218     }
219     if( list.shuffle ) {
220         // Son't need to worry about the determinism check here because it only
221         // affects audio, not game logic.
222         // NOLINTNEXTLINE(cata-determinism)
223         static auto eng = cata_default_random_engine(
224                               std::chrono::system_clock::now().time_since_epoch().count() );
225         std::shuffle( playlist_indexes.begin(), playlist_indexes.end(), eng );
226     }
227 
228     current_playlist = playlist;
229     current_playlist_at = playlist_indexes.at( absolute_playlist_at );
230 
231     const music_playlist::entry &next = list.entries[current_playlist_at];
232     current_music_track_volume = next.volume;
233     play_music_file( next.file, next.volume );
234 }
235 
stop_music()236 void stop_music()
237 {
238     if( test_mode ) {
239         return;
240     }
241 
242     Mix_FreeMusic( current_music );
243     Mix_HaltMusic();
244     current_music = nullptr;
245 
246     current_playlist.clear();
247     current_playlist_at = 0;
248     absolute_playlist_at = 0;
249 }
250 
update_music_volume()251 void update_music_volume()
252 {
253     if( test_mode ) {
254         return;
255     }
256 
257     sounds::sound_enabled = ::get_option<bool>( "SOUND_ENABLED" );
258 
259     if( !sounds::sound_enabled ) {
260         stop_music();
261         return;
262     }
263 
264     Mix_VolumeMusic( current_music_track_volume * get_option<int>( "MUSIC_VOLUME" ) / 100 );
265     // Start playing music, if we aren't already doing so (if
266     // SOUND_ENABLED was toggled.)
267 
268     // needs to be changed to something other than a static string when
269     // #28018 is resolved, as this function may be called from places
270     // other than the main menu.
271     play_music( "title" );
272 }
273 
274 // Allocate new Mix_Chunk as a null-chunk. Results in a valid, but empty chunk
275 // that is created when loading of a sound effect resource fails. Does not own
276 // memory. Mix_FreeChunk will free the SDL_malloc'd Mix_Chunk pointer.
make_null_chunk()277 static Mix_Chunk *make_null_chunk()
278 {
279     static Mix_Chunk null_chunk = { 0, nullptr, 0, 0 };
280     // SDL_malloc to match up with Mix_FreeChunk's SDL_free call
281     // to free the Mix_Chunk object memory
282     Mix_Chunk *nchunk = static_cast<Mix_Chunk *>( SDL_malloc( sizeof( Mix_Chunk ) ) );
283 
284     // Assign as copy of null_chunk
285     ( *nchunk ) = null_chunk;
286     return nchunk;
287 }
288 
load_chunk(const std::string & path)289 static Mix_Chunk *load_chunk( const std::string &path )
290 {
291     Mix_Chunk *result = Mix_LoadWAV( path.c_str() );
292     if( result == nullptr ) {
293         // Failing to load a sound file is not a fatal error worthy of a backtrace
294         dbg( D_WARNING ) << "Failed to load sfx audio file " << path << ": " << Mix_GetError();
295         result = make_null_chunk();
296     }
297     return result;
298 }
299 
300 // Check to see if the resource has already been loaded
301 // - Loaded: Return stored pointer
302 // - Not Loaded: Load chunk from stored resource path
get_sfx_resource(int resource_id)303 static inline Mix_Chunk *get_sfx_resource( int resource_id )
304 {
305     sound_effect_resource &resource = sfx_resources.resource[ resource_id ];
306     if( !resource.chunk ) {
307         std::string path = ( current_soundpack_path + "/" + resource.path );
308         resource.chunk.reset( load_chunk( path ) );
309     }
310     return resource.chunk.get();
311 }
312 
add_sfx_path(const std::string & path)313 static inline int add_sfx_path( const std::string &path )
314 {
315     auto find_result = unique_paths.find( path );
316     if( find_result != unique_paths.end() ) {
317         return find_result->second;
318     } else {
319         int result = sfx_resources.resource.size();
320         sound_effect_resource new_resource;
321         new_resource.path = path;
322         new_resource.chunk.reset();
323         sfx_resources.resource.push_back( std::move( new_resource ) );
324         unique_paths[ path ] = result;
325         return result;
326     }
327 }
328 
load_sound_effects(const JsonObject & jsobj)329 void sfx::load_sound_effects( const JsonObject &jsobj )
330 {
331     if( !sound_init_success ) {
332         return;
333     }
334     const id_and_variant key( jsobj.get_string( "id" ), jsobj.get_string( "variant", "default" ) );
335     const int volume = jsobj.get_int( "volume", 100 );
336     auto &effects = sfx_resources.sound_effects[ key ];
337 
338     for( const std::string file : jsobj.get_array( "files" ) ) {
339         sound_effect new_sound_effect;
340         new_sound_effect.volume = volume;
341         new_sound_effect.resource_id = add_sfx_path( file );
342 
343         effects.push_back( new_sound_effect );
344     }
345 }
load_sound_effect_preload(const JsonObject & jsobj)346 void sfx::load_sound_effect_preload( const JsonObject &jsobj )
347 {
348     if( !sound_init_success ) {
349         return;
350     }
351 
352     for( JsonObject aobj : jsobj.get_array( "preload" ) ) {
353         const id_and_variant preload_key( aobj.get_string( "id" ), aobj.get_string( "variant",
354                                           "default" ) );
355         sfx_preload.push_back( preload_key );
356     }
357 }
358 
load_playlist(const JsonObject & jsobj)359 void sfx::load_playlist( const JsonObject &jsobj )
360 {
361     if( !sound_init_success ) {
362         return;
363     }
364 
365     for( JsonObject playlist : jsobj.get_array( "playlists" ) ) {
366         const std::string playlist_id = playlist.get_string( "id" );
367         music_playlist playlist_to_load;
368         playlist_to_load.shuffle = playlist.get_bool( "shuffle", false );
369 
370         for( JsonObject entry : playlist.get_array( "files" ) ) {
371             const music_playlist::entry e{ entry.get_string( "file" ),  entry.get_int( "volume" ) };
372             playlist_to_load.entries.push_back( e );
373         }
374 
375         playlists[playlist_id] = std::move( playlist_to_load );
376     }
377 }
378 
379 // Returns a random sound effect matching given id and variant or `nullptr` if there is no
380 // matching sound effect.
find_random_effect(const id_and_variant & id_variants_pair)381 static const sound_effect *find_random_effect( const id_and_variant &id_variants_pair )
382 {
383     const auto iter = sfx_resources.sound_effects.find( id_variants_pair );
384     if( iter == sfx_resources.sound_effects.end() ) {
385         return nullptr;
386     }
387     return &random_entry_ref( iter->second );
388 }
389 
390 // Same as above, but with fallback to "default" variant. May still return `nullptr`
find_random_effect(const std::string & id,const std::string & variant)391 static const sound_effect *find_random_effect( const std::string &id, const std::string &variant )
392 {
393     const sound_effect *eff = find_random_effect( id_and_variant( id, variant ) );
394     if( eff != nullptr ) {
395         return eff;
396     }
397     return find_random_effect( id_and_variant( id, "default" ) );
398 }
399 
has_variant_sound(const std::string & id,const std::string & variant)400 bool sfx::has_variant_sound( const std::string &id, const std::string &variant )
401 {
402     return find_random_effect( id, variant ) != nullptr;
403 }
404 
405 // Deletes the dynamically created chunk (if such a chunk had been played).
cleanup_when_channel_finished(int,void * udata)406 static void cleanup_when_channel_finished( int /* channel */, void *udata )
407 {
408     Mix_Chunk *chunk = static_cast<Mix_Chunk *>( udata );
409     free( chunk->abuf );
410     free( chunk );
411 }
412 
413 // empty effect, as we cannot change the size of the output buffer,
414 // therefore we cannot do the math from do_pitch_shift here
empty_effect(int,void *,int,void *)415 static void empty_effect( int /* chan */, void * /* stream */, int /* len */, void * /* udata */ )
416 {
417 }
418 
do_pitch_shift(Mix_Chunk * s,float pitch)419 static Mix_Chunk *do_pitch_shift( Mix_Chunk *s, float pitch )
420 {
421     Uint32 s_in = s->alen / 4;
422     Uint32 s_out = static_cast<Uint32>( static_cast<float>( s_in ) * pitch );
423     float pitch_real = static_cast<float>( s_out ) / static_cast<float>( s_in );
424     Mix_Chunk *result = static_cast<Mix_Chunk *>( malloc( sizeof( Mix_Chunk ) ) );
425     result->allocated = 1;
426     result->alen = s_out * 4;
427     result->abuf = static_cast<Uint8 *>( malloc( result->alen * sizeof( Uint8 ) ) );
428     result->volume = s->volume;
429     for( Uint32 i = 0; i < s_out; i++ ) {
430         Sint16 lt = 0;
431         Sint16 rt = 0;
432         Sint16 lt_out = 0;
433         Sint16 rt_out = 0;
434         Sint64 lt_avg = 0;
435         Sint64 rt_avg = 0;
436         Uint32 begin = static_cast<Uint32>( static_cast<float>( i ) / pitch_real );
437         Uint32 end = static_cast<Uint32>( static_cast<float>( i + 1 ) / pitch_real );
438 
439         // check for boundary case
440         if( end > 0 && ( end >= ( s->alen / 4 ) ) ) {
441             end = begin;
442         }
443 
444         for( Uint32 j = begin; j <= end; j++ ) {
445             lt = ( s->abuf[( 4 * j ) + 1] << 8 ) | ( s->abuf[( 4 * j ) + 0] );
446             rt = ( s->abuf[( 4 * j ) + 3] << 8 ) | ( s->abuf[( 4 * j ) + 2] );
447             lt_avg += lt;
448             rt_avg += rt;
449         }
450         lt_out = static_cast<Sint16>( static_cast<float>( lt_avg ) / static_cast<float>
451                                       ( end - begin + 1 ) );
452         rt_out = static_cast<Sint16>( static_cast<float>( rt_avg ) / static_cast<float>
453                                       ( end - begin + 1 ) );
454         result->abuf[( 4 * i ) + 1] = static_cast<Uint8>( ( lt_out >> 8 ) & 0xFF );
455         result->abuf[( 4 * i ) + 0] = static_cast<Uint8>( lt_out & 0xFF );
456         result->abuf[( 4 * i ) + 3] = static_cast<Uint8>( ( rt_out >> 8 ) & 0xFF );
457         result->abuf[( 4 * i ) + 2] = static_cast<Uint8>( rt_out & 0xFF );
458     }
459     return result;
460 }
461 
play_variant_sound(const std::string & id,const std::string & variant,int volume)462 void sfx::play_variant_sound( const std::string &id, const std::string &variant, int volume )
463 {
464     if( test_mode ) {
465         return;
466     }
467 
468     add_msg_debug( "sound id: %s, variant: %s, volume: %d ", id, variant, volume );
469 
470     if( !check_sound( volume ) ) {
471         return;
472     }
473     const sound_effect *eff = find_random_effect( id, variant );
474     if( eff == nullptr ) {
475         eff = find_random_effect( id, "default" );
476         if( eff == nullptr ) {
477             return;
478         }
479     }
480     const sound_effect &selected_sound_effect = *eff;
481 
482     Mix_Chunk *effect_to_play = get_sfx_resource( selected_sound_effect.resource_id );
483     Mix_VolumeChunk( effect_to_play,
484                      selected_sound_effect.volume * get_option<int>( "SOUND_EFFECT_VOLUME" ) * volume / ( 100 * 100 ) );
485     bool failed = ( Mix_PlayChannel( static_cast<int>( channel::any ), effect_to_play, 0 ) == -1 );
486     if( failed ) {
487         dbg( D_ERROR ) << "Failed to play sound effect: " << Mix_GetError();
488     }
489 }
490 
play_variant_sound(const std::string & id,const std::string & variant,int volume,units::angle angle,double pitch_min,double pitch_max)491 void sfx::play_variant_sound( const std::string &id, const std::string &variant, int volume,
492                               units::angle angle, double pitch_min, double pitch_max )
493 {
494     if( test_mode ) {
495         return;
496     }
497 
498     add_msg_debug( "sound id: %s, variant: %s, volume: %d ", id, variant, volume );
499 
500     if( !check_sound( volume ) ) {
501         return;
502     }
503     const sound_effect *eff = find_random_effect( id, variant );
504     if( eff == nullptr ) {
505         return;
506     }
507     const sound_effect &selected_sound_effect = *eff;
508 
509     Mix_Chunk *effect_to_play = get_sfx_resource( selected_sound_effect.resource_id );
510     bool is_pitched = ( pitch_min > 0 ) && ( pitch_max > 0 );
511     if( is_pitched ) {
512         double pitch_random = rng_float( pitch_min, pitch_max );
513         effect_to_play = do_pitch_shift( effect_to_play, static_cast<float>( pitch_random ) );
514     }
515     Mix_VolumeChunk( effect_to_play,
516                      selected_sound_effect.volume * get_option<int>( "SOUND_EFFECT_VOLUME" ) * volume / ( 100 * 100 ) );
517     int channel = Mix_PlayChannel( static_cast<int>( sfx::channel::any ), effect_to_play, 0 );
518     bool failed = ( channel == -1 );
519     if( !failed && is_pitched ) {
520         if( Mix_RegisterEffect( channel, empty_effect, cleanup_when_channel_finished,
521                                 effect_to_play ) == 0 ) {
522             // To prevent use after free, stop the playback right now.
523             failed = true;
524             dbg( D_WARNING ) << "Mix_RegisterEffect failed: " << Mix_GetError();
525             Mix_HaltChannel( channel );
526         }
527     }
528     if( !failed ) {
529         if( Mix_SetPosition( channel, static_cast<Sint16>( to_degrees( angle ) ), 1 ) == 0 ) {
530             // Not critical
531             dbg( D_INFO ) << "Mix_SetPosition failed: " << Mix_GetError();
532         }
533     }
534     if( failed ) {
535         dbg( D_ERROR ) << "Failed to play sound effect: " << Mix_GetError();
536         if( is_pitched ) {
537             cleanup_when_channel_finished( channel, effect_to_play );
538         }
539     }
540 }
541 
play_ambient_variant_sound(const std::string & id,const std::string & variant,int volume,channel channel,int fade_in_duration,double pitch,int loops)542 void sfx::play_ambient_variant_sound( const std::string &id, const std::string &variant, int volume,
543                                       channel channel, int fade_in_duration, double pitch, int loops )
544 {
545     if( test_mode ) {
546         return;
547     }
548     if( !check_sound( volume ) ) {
549         return;
550     }
551     if( is_channel_playing( channel ) ) {
552         return;
553     }
554     const sound_effect *eff = find_random_effect( id, variant );
555     if( eff == nullptr ) {
556         return;
557     }
558     const sound_effect &selected_sound_effect = *eff;
559 
560     Mix_Chunk *effect_to_play = get_sfx_resource( selected_sound_effect.resource_id );
561     bool is_pitched = ( pitch > 0 );
562     if( is_pitched ) {
563         effect_to_play = do_pitch_shift( effect_to_play, static_cast<float>( pitch ) );
564     }
565     Mix_VolumeChunk( effect_to_play,
566                      selected_sound_effect.volume * get_option<int>( "AMBIENT_SOUND_VOLUME" ) * volume / ( 100 * 100 ) );
567     bool failed = false;
568     int ch = static_cast<int>( channel );
569     if( fade_in_duration ) {
570         failed = ( Mix_FadeInChannel( ch, effect_to_play, loops, fade_in_duration ) == -1 );
571     } else {
572         failed = ( Mix_PlayChannel( ch, effect_to_play, loops ) == -1 );
573     }
574     if( !failed && is_pitched ) {
575         if( Mix_RegisterEffect( ch, empty_effect, cleanup_when_channel_finished, effect_to_play ) == 0 ) {
576             // To prevent use after free, stop the playback right now.
577             failed = true;
578             dbg( D_WARNING ) << "Mix_RegisterEffect failed: " << Mix_GetError();
579             Mix_HaltChannel( ch );
580         }
581     }
582     if( failed ) {
583         dbg( D_ERROR ) << "Failed to play sound effect: " << Mix_GetError();
584         if( is_pitched ) {
585             cleanup_when_channel_finished( ch, effect_to_play );
586         }
587     }
588 }
589 
load_soundset()590 void load_soundset()
591 {
592     const std::string default_path = PATH_INFO::defaultsounddir();
593     const std::string default_soundpack = "basic";
594     std::string current_soundpack = get_option<std::string>( "SOUNDPACKS" );
595     std::string soundpack_path;
596 
597     // Get current soundpack and it's directory path.
598     if( current_soundpack.empty() ) {
599         dbg( D_ERROR ) << "Soundpack not set in options or empty.";
600         soundpack_path = default_path;
601         current_soundpack = default_soundpack;
602     } else {
603         dbg( D_INFO ) << "Current soundpack is: " << current_soundpack;
604         soundpack_path = SOUNDPACKS[current_soundpack];
605     }
606 
607     if( soundpack_path.empty() ) {
608         dbg( D_ERROR ) << "Soundpack with name " << current_soundpack << " can't be found or empty string";
609         soundpack_path = default_path;
610         current_soundpack = default_soundpack;
611     } else {
612         dbg( D_INFO ) << '"' << current_soundpack << '"' << " soundpack: found path: " << soundpack_path;
613     }
614 
615     current_soundpack_path = soundpack_path;
616     try {
617         loading_ui ui( false );
618         DynamicDataLoader::get_instance().load_data_from_path( soundpack_path, "core", ui );
619     } catch( const std::exception &err ) {
620         dbg( D_ERROR ) << "failed to load sounds: " << err.what();
621     }
622 
623     // Preload sound effects
624     for( const id_and_variant &preload : sfx_preload ) {
625         const auto find_result = sfx_resources.sound_effects.find( preload );
626         if( find_result != sfx_resources.sound_effects.end() ) {
627             for( const auto &sfx : find_result->second ) {
628                 get_sfx_resource( sfx.resource_id );
629             }
630         }
631     }
632 
633     // Memory of unique_paths no longer required, swap with locally scoped unordered_map
634     // to force deallocation of resources.
635     {
636         unique_paths.clear();
637         std::unordered_map<std::string, int> t_swap;
638         unique_paths.swap( t_swap );
639     }
640     // Memory of sfx_preload no longer required, swap with locally scoped vector
641     // to force deallocation of resources.
642     {
643         sfx_preload.clear();
644         std::vector<id_and_variant> t_swap;
645         sfx_preload.swap( t_swap );
646     }
647 }
648 
649 #endif
650