The Battle for Wesnoth  1.19.15+dev
sound.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #include "sound.hpp"
17 #include "filesystem.hpp"
18 #include "log.hpp"
20 #include "random.hpp"
22 #include "sound_music_track.hpp"
23 #include "utils/general.hpp"
24 #include "utils/rate_counter.hpp"
25 
26 #include <SDL2/SDL.h>
27 #include <SDL2/SDL_mixer.h>
28 
29 #include <list>
30 #include <string>
31 #include <utility>
32 
33 static lg::log_domain log_audio("audio");
34 #define DBG_AUDIO LOG_STREAM(debug, log_audio)
35 #define LOG_AUDIO LOG_STREAM(info, log_audio)
36 #define ERR_AUDIO LOG_STREAM(err, log_audio)
37 
38 namespace sound
39 {
40 // Channel-chunk mapping lets us know, if we can safely free a given chunk
41 static std::vector<Mix_Chunk*> channel_chunks;
42 
43 // Channel-id mapping for use with sound sources (to check if given source
44 // is playing on a channel for fading/panning)
45 static std::vector<int> channel_ids;
46 }
47 
48 using namespace std::chrono_literals;
49 
50 namespace
51 {
52 bool mix_ok = false;
53 utils::optional<std::chrono::steady_clock::time_point> music_start_time;
54 utils::rate_counter music_refresh_rate{20};
55 bool want_new_music = false;
56 auto fade_out_time = 5000ms;
57 bool no_fading = false;
58 bool unload_music = false;
59 
60 // number of allocated channels,
61 const std::size_t n_of_channels = 32;
62 
63 // we need 2 channels, because we it for timer as well
64 const std::size_t bell_channel = 0;
65 const std::size_t timer_channel = 1;
66 
67 // number of channels reserved for sound sources
68 const std::size_t source_channels = 8;
69 const std::size_t source_channel_start = timer_channel + 1;
70 const std::size_t source_channel_last = source_channel_start + source_channels - 1;
71 const std::size_t UI_sound_channels = 2;
72 const std::size_t UI_sound_channel_start = source_channel_last + 1;
73 const std::size_t UI_sound_channel_last = UI_sound_channel_start + UI_sound_channels - 1;
74 const std::size_t n_reserved_channels = UI_sound_channel_last + 1; // sources, bell, timer and UI
75 
76 // Max number of sound chunks that we want to cache
77 // Keep this above number of available channels to avoid busy-looping
78 unsigned max_cached_chunks = 256;
79 
80 std::map<Mix_Chunk*, int> chunk_usage;
81 } // end anon namespace
82 
83 static void increment_chunk_usage(Mix_Chunk* mcp)
84 {
85  ++(chunk_usage[mcp]);
86 }
87 
88 static void decrement_chunk_usage(Mix_Chunk* mcp)
89 {
90  if(mcp == nullptr) {
91  return;
92  }
93 
94  std::map<Mix_Chunk*, int>::iterator this_usage = chunk_usage.find(mcp);
95  assert(this_usage != chunk_usage.end());
96  if(--(this_usage->second) == 0) {
97  Mix_FreeChunk(mcp);
98  chunk_usage.erase(this_usage);
99  }
100 }
101 
102 namespace
103 {
104 class sound_cache_chunk
105 {
106 public:
107  sound_cache_chunk(const std::string& f)
108  : group(sound::NULL_CHANNEL)
109  , file(f)
110  , data_(nullptr)
111  {
112  }
113  sound_cache_chunk(const sound_cache_chunk& scc)
114  : group(scc.group)
115  , file(scc.file)
116  , data_(scc.data_)
117  {
118  increment_chunk_usage(data_);
119  }
120 
121  ~sound_cache_chunk()
122  {
123  decrement_chunk_usage(data_);
124  }
125 
126  sound::channel_group group;
127  std::string file;
128 
129  void set_data(Mix_Chunk* d)
130  {
132  decrement_chunk_usage(data_);
133  data_ = d;
134  }
135 
136  Mix_Chunk* get_data() const
137  {
138  return data_;
139  }
140 
141  bool operator==(const sound_cache_chunk& scc) const
142  {
143  return file == scc.file;
144  }
145 
146  bool operator!=(const sound_cache_chunk& scc) const
147  {
148  return !operator==(scc);
149  }
150 
151  sound_cache_chunk& operator=(const sound_cache_chunk& scc)
152  {
153  file = scc.file;
154  group = scc.group;
155  set_data(scc.get_data());
156  return *this;
157  }
158 
159 private:
160  Mix_Chunk* data_;
161 };
162 
163 std::list<sound_cache_chunk> sound_cache;
164 typedef std::list<sound_cache_chunk>::iterator sound_cache_iterator;
165 std::map<std::string, std::shared_ptr<Mix_Music>> music_cache;
166 
167 std::vector<std::string> played_before;
168 
169 //
170 // FIXME: the first music_track may be initialized before main()
171 // is reached. Using the logging facilities may lead to a SIGSEGV
172 // because it's not guaranteed that their objects are already alive.
173 //
174 // Use the music_track default constructor to avoid trying to
175 // invoke a log object while resolving paths.
176 //
177 std::vector<std::shared_ptr<sound::music_track>> current_track_list;
178 std::shared_ptr<sound::music_track> current_track;
179 unsigned int current_track_index = 0;
180 std::shared_ptr<sound::music_track> previous_track;
181 
182 std::vector<std::shared_ptr<sound::music_track>>::const_iterator find_track(const sound::music_track& track)
183 {
184  return utils::ranges::find(current_track_list, track,
185  [](const std::shared_ptr<const sound::music_track>& ptr) { return *ptr; });
186 }
187 
188 } // end anon namespace
189 
190 namespace sound
191 {
193 {
194  sound_cache.clear();
195  music_cache.clear();
196 }
197 
198 utils::optional<unsigned int> get_current_track_index()
199 {
200  if(current_track_index >= current_track_list.size()){
201  return {};
202  }
203  return current_track_index;
204 }
205 std::shared_ptr<music_track> get_current_track()
206 {
207  return current_track;
208 }
209 std::shared_ptr<music_track> get_previous_music_track()
210 {
211  return previous_track;
212 }
213 void set_previous_track(std::shared_ptr<music_track> track)
214 {
215  previous_track = std::move(track);
216 }
217 
218 unsigned int get_num_tracks()
219 {
220  return current_track_list.size();
221 }
222 
223 std::shared_ptr<music_track> get_track(unsigned int i)
224 {
225  if(i < current_track_list.size()) {
226  return current_track_list[i];
227  }
228 
229  if(i == current_track_list.size()) {
230  return current_track;
231  }
232 
233  return nullptr;
234 }
235 
236 void set_track(unsigned int i, const std::shared_ptr<music_track>& to)
237 {
238  if(i < current_track_list.size() && find_track(*to) != current_track_list.end()) {
239  current_track_list[i] = std::make_shared<music_track>(*to);
240  }
241 }
242 
243 void remove_track(unsigned int i)
244 {
245  if(i >= current_track_list.size()) {
246  return;
247  }
248 
249  if(i == current_track_index) {
250  // Let the track finish playing
251  if(current_track){
252  current_track->set_play_once(true);
253  }
254  // Set current index to the new size of the list
255  current_track_index = current_track_list.size() - 1;
256  } else if(i < current_track_index) {
257  current_track_index--;
258  }
259 
260  current_track_list.erase(current_track_list.begin() + i);
261 }
262 
263 } // end namespace sound
264 
265 static bool track_ok(const std::string& id)
266 {
267  LOG_AUDIO << "Considering " << id;
268 
269  if(!current_track) {
270  return true;
271  }
272 
273  // If they committed changes to list, we forget previous plays, but
274  // still *never* repeat same track twice if we have an option.
275  if(id == current_track->file_path()) {
276  return false;
277  }
278 
279  if(current_track_list.size() <= 3) {
280  return true;
281  }
282 
283  // Timothy Pinkham says:
284  // 1) can't be repeated without 2 other pieces have already played
285  // since A was played.
286  // 2) cannot play more than 2 times without every other piece
287  // having played at least 1 time.
288 
289  // Dammit, if our musicians keep coming up with algorithms, I'll
290  // be out of a job!
291  unsigned int num_played = 0;
292  std::set<std::string> played;
293  std::vector<std::string>::reverse_iterator i;
294 
295  for(i = played_before.rbegin(); i != played_before.rend(); ++i) {
296  if(*i == id) {
297  ++num_played;
298  if(num_played == 2) {
299  break;
300  }
301  } else {
302  played.insert(*i);
303  }
304  }
305 
306  // If we've played this twice, must have played every other track.
307  if(num_played == 2 && played.size() != current_track_list.size() - 1) {
308  LOG_AUDIO << "Played twice with only " << played.size() << " tracks between";
309  return false;
310  }
311 
312  // Check previous previous track not same.
313  i = played_before.rbegin();
314  if(i != played_before.rend()) {
315  ++i;
316  if(i != played_before.rend()) {
317  if(*i == id) {
318  LOG_AUDIO << "Played just before previous";
319  return false;
320  }
321  }
322  }
323 
324  return true;
325 }
326 
327 static std::shared_ptr<sound::music_track> choose_track()
328 {
329  assert(!current_track_list.empty());
330 
331  if(current_track_index >= current_track_list.size()) {
332  current_track_index = 0;
333  }
334 
335  if(current_track_list[current_track_index]->shuffle()) {
336  unsigned int track = 0;
337 
338  if(current_track_list.size() > 1) {
339  do {
340  track = randomness::rng::default_instance().get_random_int(0, current_track_list.size()-1);
341  } while(!track_ok(current_track_list[track]->file_path()));
342  }
343 
344  current_track_index = track;
345  }
346 
347  DBG_AUDIO << "Next track will be " << current_track_list[current_track_index]->file_path();
348  played_before.push_back(current_track_list[current_track_index]->file_path());
349  return current_track_list[current_track_index];
350 }
351 
352 static std::string pick_one(const std::string& files)
353 {
354  std::vector<std::string> ids = utils::square_parenthetical_split(files, ',', "[", "]");
355 
356  if(ids.empty()) {
357  return "";
358  }
359 
360  if(ids.size() == 1) {
361  return ids[0];
362  }
363 
364  // We avoid returning same choice twice if we can avoid it.
365  static std::map<std::string, unsigned int> prev_choices;
366  unsigned int choice;
367 
368  if(prev_choices.find(files) != prev_choices.end()) {
369  choice = randomness::rng::default_instance().get_random_int(0, ids.size()-1 - 1);
370  if(choice >= prev_choices[files]) {
371  ++choice;
372  }
373 
374  prev_choices[files] = choice;
375  } else {
376  choice = randomness::rng::default_instance().get_random_int(0, ids.size()-1);
377  prev_choices.emplace(files, choice);
378  }
379 
380  return ids[choice];
381 }
382 
383 namespace
384 {
385 struct audio_lock
386 {
387  audio_lock()
388  {
389  SDL_LockAudio();
390  }
391 
392  ~audio_lock()
393  {
394  SDL_UnlockAudio();
395  }
396 };
397 
398 } // end of anonymous namespace
399 
400 namespace sound
401 {
402 // Removes channel-chunk and channel-id mapping
404 {
405  channel_chunks[channel] = nullptr;
406  channel_ids[channel] = -1;
407 }
408 
409 std::string current_driver()
410 {
411  const char* const drvname = SDL_GetCurrentAudioDriver();
412  return drvname ? drvname : "<not initialized>";
413 }
414 
415 std::vector<std::string> enumerate_drivers()
416 {
417  std::vector<std::string> res;
418  int num_drivers = SDL_GetNumVideoDrivers();
419 
420  for(int n = 0; n < num_drivers; ++n) {
421  const char* drvname = SDL_GetAudioDriver(n);
422  res.emplace_back(drvname ? drvname : "<invalid driver>");
423  }
424 
425  return res;
426 }
427 
428 driver_status driver_status::query()
429 {
430  driver_status res{mix_ok, 0, 0, 0, 0};
431 
432  if(mix_ok) {
433  Mix_QuerySpec(&res.frequency, &res.format, &res.channels);
434  res.chunk_size = prefs::get().sound_buffer_size();
435  }
436 
437  return res;
438 }
439 
441 {
442  LOG_AUDIO << "Initializing audio...";
443  if(SDL_WasInit(SDL_INIT_AUDIO) == 0) {
444  if(SDL_InitSubSystem(SDL_INIT_AUDIO) == -1) {
445  return false;
446  }
447  }
448 
449  if(!mix_ok) {
450  if(Mix_OpenAudio(prefs::get().sample_rate(), MIX_DEFAULT_FORMAT, 2, prefs::get().sound_buffer_size()) == -1) {
451  mix_ok = false;
452  ERR_AUDIO << "Could not initialize audio: " << Mix_GetError();
453  return false;
454  }
455 
456  mix_ok = true;
457  Mix_AllocateChannels(n_of_channels);
458  Mix_ReserveChannels(n_reserved_channels);
459 
460  channel_chunks.clear();
461  channel_chunks.resize(n_of_channels, nullptr);
462  channel_ids.resize(n_of_channels, -1);
463 
464  Mix_GroupChannel(bell_channel, SOUND_BELL);
465  Mix_GroupChannel(timer_channel, SOUND_TIMER);
466  Mix_GroupChannels(source_channel_start, source_channel_last, SOUND_SOURCES);
467  Mix_GroupChannels(UI_sound_channel_start, UI_sound_channel_last, SOUND_UI);
468  Mix_GroupChannels(n_reserved_channels, n_of_channels - 1, SOUND_FX);
469 
474 
475  Mix_ChannelFinished(channel_finished_hook);
476 
477  LOG_AUDIO << "Audio initialized.";
478 
479  DBG_AUDIO << "Channel layout: " << n_of_channels << " channels (" << n_reserved_channels << " reserved)\n"
480  << " " << bell_channel << " - bell\n"
481  << " " << timer_channel << " - timer\n"
482  << " " << source_channel_start << ".." << source_channel_last << " - sound sources\n"
483  << " " << UI_sound_channel_start << ".." << UI_sound_channel_last << " - UI\n"
484  << " " << UI_sound_channel_last + 1 << ".." << n_of_channels - 1 << " - sound effects";
485 
486  play_music();
487  }
488 
489  return true;
490 }
491 
493 {
494  int frequency, channels;
495  uint16_t format;
496 
497  if(mix_ok) {
498  stop_bell();
499  stop_UI_sound();
500  stop_sound();
501  sound_cache.clear();
502  stop_music();
503  mix_ok = false;
504 
505  int numtimesopened = Mix_QuerySpec(&frequency, &format, &channels);
506  if(numtimesopened == 0) {
507  ERR_AUDIO << "Error closing audio device: " << Mix_GetError();
508  }
509 
510  while(numtimesopened) {
511  Mix_CloseAudio();
512  --numtimesopened;
513  }
514  }
515 
516  if(SDL_WasInit(SDL_INIT_AUDIO) != 0) {
517  SDL_QuitSubSystem(SDL_INIT_AUDIO);
518  }
519 
520  LOG_AUDIO << "Audio device released.";
521 }
522 
524 {
525  bool music = prefs::get().music_on();
526  bool sound = prefs::get().sound();
527  bool UI_sound = prefs::get().ui_sound_on();
528  bool bell = prefs::get().turn_bell();
529 
530  if(music || sound || bell || UI_sound) {
532  if(!sound::init_sound()) {
533  ERR_AUDIO << "Error initializing audio device: " << Mix_GetError();
534  }
535 
536  if(!music) {
538  }
539 
540  if(!sound) {
542  }
543 
544  if(!UI_sound) {
546  }
547 
548  if(!bell) {
550  }
551  }
552 }
553 
555 {
556  if(mix_ok) {
557  Mix_FadeOutMusic(500);
558  Mix_HookMusicFinished([]() { unload_music = true; });
559  }
560 }
561 
563 {
564  if(mix_ok) {
565  Mix_HaltGroup(SOUND_SOURCES);
566  Mix_HaltGroup(SOUND_FX);
567 
568  sound_cache.remove_if([](const sound_cache_chunk& c) {
569  return c.group == SOUND_SOURCES || c.group == SOUND_FX;
570  });
571  }
572 }
573 
574 /*
575  * For the purpose of channel manipulation, we treat turn timer the same as bell
576  */
577 void stop_bell()
578 {
579  if(mix_ok) {
580  Mix_HaltGroup(SOUND_BELL);
581  Mix_HaltGroup(SOUND_TIMER);
582 
583  sound_cache.remove_if([](const sound_cache_chunk& c) {
584  return c.group == SOUND_BELL || c.group == SOUND_TIMER;
585  });
586  }
587 }
588 
590 {
591  if(mix_ok) {
592  Mix_HaltGroup(SOUND_UI);
593 
594  sound_cache.remove_if([](const sound_cache_chunk& c) {
595  return c.group == SOUND_UI;
596  });
597  }
598 }
599 
600 void play_music_once(const std::string& file)
601 {
602  if(auto track = sound::music_track::create(file)) {
603  set_previous_track(current_track);
604  current_track = std::move(track);
605  current_track->set_play_once(true);
606  current_track_index = current_track_list.size();
607  play_music();
608  }
609 }
610 
612 {
613  current_track_list.clear();
614 }
615 
617 {
618  if(!current_track) {
619  return;
620  }
621 
622  music_start_time = std::chrono::steady_clock::now(); // immediate
623  want_new_music = true;
624  no_fading = false;
625  fade_out_time = previous_track != nullptr ? previous_track->ms_after() : 0ms;
626 }
627 
628 void play_track(unsigned int i)
629 {
630  set_previous_track(current_track);
631  if(i >= current_track_list.size()) {
632  current_track = choose_track();
633  } else {
634  current_track_index = i;
635  current_track = current_track_list[i];
636  }
637  play_music();
638 }
639 
640 static void play_new_music()
641 {
642  music_start_time.reset(); // reset status: no start time
643  want_new_music = true;
644 
645  if(!prefs::get().music_on() || !mix_ok || !current_track) {
646  return;
647  }
648 
649  std::string filename = current_track->file_path();
650 
651  auto itor = music_cache.find(filename);
652  if(itor == music_cache.end()) {
653  LOG_AUDIO << "attempting to insert track '" << filename << "' into cache";
654 
656  // SDL takes ownership of rwops
657  const std::shared_ptr<Mix_Music> music(Mix_LoadMUSType_RW(rwops.release(), MUS_NONE, true), &Mix_FreeMusic);
658 
659  if(music == nullptr) {
660  ERR_AUDIO << "Could not load music file '" << filename << "': " << Mix_GetError();
661  return;
662  }
663 
664  itor = music_cache.emplace(filename, music).first;
665  }
666 
667  LOG_AUDIO << "Playing track '" << filename << "'";
668  auto fading_time = current_track->ms_before();
669  if(no_fading) {
670  fading_time = 0ms;
671  }
672 
673  // Halt any existing music.
674  // If we don't do this SDL_Mixer blocks everything until fade out is complete.
675  // Do not remove this without ensuring that it does not block.
676  // If you don't want it to halt the music, ensure that fades are completed
677  // before attempting to play new music.
678  Mix_HaltMusic();
679 
680  // Fade in the new music
681  const int res = Mix_FadeInMusic(itor->second.get(), 1, fading_time.count());
682  if(res < 0) {
683  ERR_AUDIO << "Could not play music: " << Mix_GetError() << " " << filename << " ";
684  }
685 
686  want_new_music = false;
687 }
688 
689 void play_music_config(const config& music_node, bool allow_interrupt_current_track, int i)
690 {
691  //
692  // FIXME: there is a memory leak somewhere in this function, seemingly related to the shared_ptrs
693  // stored in current_track_list.
694  //
695  // vultraz 5/8/2017
696  // vultraz 2025-07-19 function has been updated, can someone check again
697  //
698 
699  auto track = sound::music_track::create(music_node);
700  if(!track) {
701  ERR_AUDIO << "cannot open track; disabled in this playlist.";
702  return;
703  }
704 
705  // If they say play once, we don't alter playlist.
706  if(track->play_once()) {
707  set_previous_track(current_track);
708  current_track = std::move(track);
709  current_track_index = current_track_list.size();
710  play_music();
711  return;
712  }
713 
714  // Clear play list unless they specify append.
715  if(!track->append()) {
716  current_track_list.clear();
717  }
718 
719  auto iter = find_track(*track);
720  // Avoid 2 tracks with the same name, since that can cause an infinite loop
721  // in choose_track(), 2 tracks with the same name will always return the
722  // current track and track_ok() doesn't allow that.
723  if(iter == current_track_list.end()) {
724  auto insert_at = (i >= 0 && static_cast<std::size_t>(i) < current_track_list.size())
725  ? current_track_list.begin() + i
726  : current_track_list.end();
727 
728  // Copy the track pointer so our local variable remains non-null.
729  iter = current_track_list.insert(insert_at, track);
730  auto new_track_index = std::distance(current_track_list.cbegin(), iter);
731 
732  // If we inserted the new track *before* the current track, adjust
733  // cached index so it still points to the same element.
734  if(new_track_index <= current_track_index) {
735  ++current_track_index;
736  }
737  } else {
738  ERR_AUDIO << "tried to add duplicate track '" << track->file_path() << "'";
739  }
740 
741  // They can tell us to start playing this list immediately.
742  if(track->immediate()) {
743  set_previous_track(current_track);
744  current_track = *iter;
745  current_track_index = std::distance(current_track_list.cbegin(), iter);
746  play_music();
747  } else if(!track->append() && !allow_interrupt_current_track && current_track) {
748  // Make sure the current track will finish first
749  current_track->set_play_once(true);
750  }
751 }
752 
754 {
755  if(Mix_FadingMusic() != MIX_NO_FADING) {
756  // Do not block everything while fading.
757  return;
758  }
759 
760  if(prefs::get().music_on()) {
761  // TODO: rethink the music_thinker design, especially the use of fade_out_time
762  auto now = std::chrono::steady_clock::now();
763 
764  if(!music_start_time && !current_track_list.empty() && !Mix_PlayingMusic()) {
765  // Pick next track, add ending time to its start time.
766  set_previous_track(current_track);
767  current_track = choose_track();
768  music_start_time = now;
769  no_fading = true;
770  fade_out_time = 0ms;
771  }
772 
773  if(music_start_time && music_refresh_rate.poll()) {
774  want_new_music = now >= *music_start_time - fade_out_time;
775  }
776 
777  if(want_new_music) {
778  if(Mix_PlayingMusic()) {
779  Mix_FadeOutMusic(fade_out_time.count());
780  return;
781  }
782 
783  unload_music = false;
784  play_new_music();
785  }
786  }
787 
788  if(unload_music) {
789  // The custom shared_ptr deleter (Mix_FreeMusic) will handle the freeing of each track.
790  music_cache.clear();
791 
792  Mix_HookMusicFinished(nullptr);
793 
794  unload_music = false;
795  }
796 }
797 
798 music_muter::music_muter()
799  : events::sdl_handler(false)
800 {
801  join_global();
802 }
803 
804 void music_muter::handle_window_event(const SDL_Event& event)
805 {
806  if(prefs::get().stop_music_in_background() && prefs::get().music_on()) {
807  if(event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
808  Mix_ResumeMusic();
809  } else if(event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
810  if(Mix_PlayingMusic()) {
811  Mix_PauseMusic();
812  }
813  }
814  }
815 }
816 
818 {
819  played_before.clear();
820 
821  // Play-once is OK if still playing.
822  if(current_track) {
823  if(current_track->play_once()) {
824  return;
825  }
826 
827  // If current track no longer on playlist, change it.
828  for(auto m : current_track_list) {
829  if(*current_track == *m) {
830  return;
831  }
832  }
833  }
834 
835  // Victory empties playlist: if next scenario doesn't specify one...
836  if(current_track_list.empty()) {
837  return;
838  }
839 
840  // FIXME: we don't pause ms_before on this first track. Should we?
841  set_previous_track(current_track);
842  current_track = choose_track();
843  play_music();
844 }
845 
847 {
848  // First entry clears playlist, others append to it.
849  bool append = false;
850  for(auto m : current_track_list) {
851  m->write(snapshot, append);
852  append = true;
853  }
854 }
855 
856 void reposition_sound(int id, unsigned int distance)
857 {
858  audio_lock lock;
859  for(unsigned ch = 0; ch < channel_ids.size(); ++ch) {
860  if(channel_ids[ch] != id) {
861  continue;
862  }
863 
864  if(distance >= DISTANCE_SILENT) {
865  Mix_HaltChannel(ch);
866  } else {
867  Mix_SetDistance(ch, distance);
868  }
869  }
870 }
871 
872 bool is_sound_playing(int id)
873 {
874  audio_lock lock;
875  return utils::contains(channel_ids, id);
876 }
877 
878 void stop_sound(int id)
879 {
881 }
882 
883 namespace
884 {
885 struct chunk_load_exception
886 {
887 };
888 
889 Mix_Chunk* load_chunk(const std::string& file, channel_group group)
890 {
891  sound_cache_iterator it_bgn, it_end;
892  sound_cache_iterator it;
893 
894  sound_cache_chunk temp_chunk(file); // search the sound cache on this key
895  it_bgn = sound_cache.begin();
896  it_end = sound_cache.end();
897  it = std::find(it_bgn, it_end, temp_chunk);
898 
899  if(it != it_end) {
900  if(it->group != group) {
901  // cached item has been used in multiple sound groups
902  it->group = NULL_CHANNEL;
903  }
904 
905  // splice the most recently used chunk to the front of the cache
906  sound_cache.splice(it_bgn, sound_cache, it);
907  } else {
908  // remove the least recently used chunk from cache if it's full
909  bool cache_full = (sound_cache.size() == max_cached_chunks);
910  while(cache_full && it != it_bgn) {
911  // make sure this chunk is not being played before freeing it
912  if(!utils::contains(channel_chunks, (--it)->get_data())) {
913  sound_cache.erase(it);
914  cache_full = false;
915  }
916  }
917 
918  if(cache_full) {
919  LOG_AUDIO << "Maximum sound cache size reached and all are busy, skipping.";
920  throw chunk_load_exception();
921  }
922 
923  temp_chunk.group = group;
924  const auto filename = filesystem::get_binary_file_location("sounds", file);
925  const auto localized = filesystem::get_localized_path(filename.value_or(""));
926 
927  if(filename) {
928  filesystem::rwops_ptr rwops = filesystem::make_read_RWops(localized.value_or(filename.value()));
929  temp_chunk.set_data(Mix_LoadWAV_RW(rwops.release(), true)); // SDL takes ownership of rwops
930  } else {
931  ERR_AUDIO << "Could not load sound file '" << file << "'.";
932  throw chunk_load_exception();
933  }
934 
935  if(temp_chunk.get_data() == nullptr) {
936  ERR_AUDIO << "Could not load sound file '" << filename.value() << "': " << Mix_GetError();
937  throw chunk_load_exception();
938  }
939 
940  sound_cache.push_front(temp_chunk);
941  }
942 
943  return sound_cache.begin()->get_data();
944 }
945 
946 using namespace std::chrono_literals;
947 
948 void play_sound_internal(const std::string& files,
949  channel_group group,
950  unsigned int repeats = 0,
951  unsigned int distance = 0,
952  int id = -1,
953  const std::chrono::milliseconds& loop_ticks = 0ms,
954  const std::chrono::milliseconds& fadein_ticks = 0ms)
955 {
956  if(files.empty() || distance >= DISTANCE_SILENT || !mix_ok) {
957  return;
958  }
959 
960  audio_lock lock;
961 
962  // find a free channel in the desired group
963  int channel = Mix_GroupAvailable(group);
964  if(channel == -1) {
965  LOG_AUDIO << "All channels dedicated to sound group(" << group << ") are busy, skipping.";
966  return;
967  }
968 
969  Mix_Chunk* chunk;
970  std::string file = pick_one(files);
971 
972  try {
973  chunk = load_chunk(file, group);
974  assert(chunk);
975  } catch(const chunk_load_exception&) {
976  return;
977  }
978 
979  /*
980  * This check prevents SDL_Mixer from blowing up on Windows when UI sound is played
981  * in response to toggling the checkbox which disables sound.
982  */
983  if(group != SOUND_UI) {
984  Mix_SetDistance(channel, distance);
985  }
986 
987  int res;
988  if(loop_ticks > 0ms) {
989  if(fadein_ticks > 0ms) {
990  res = Mix_FadeInChannelTimed(channel, chunk, -1, fadein_ticks.count(), loop_ticks.count());
991  } else {
992  res = Mix_PlayChannel(channel, chunk, -1);
993  }
994 
995  if(res >= 0) {
996  Mix_ExpireChannel(channel, loop_ticks.count());
997  }
998  } else {
999  if(fadein_ticks > 0ms) {
1000  res = Mix_FadeInChannel(channel, chunk, repeats, fadein_ticks.count());
1001  } else {
1002  res = Mix_PlayChannel(channel, chunk, repeats);
1003  }
1004  }
1005 
1006  if(res < 0) {
1007  ERR_AUDIO << "error playing sound effect: " << Mix_GetError();
1008  // still keep it in the sound cache, in case we want to try again later
1009  return;
1010  }
1011 
1012  channel_ids[channel] = id;
1013 
1014  // reserve the channel's chunk from being freed, since it is playing
1015  channel_chunks[res] = chunk;
1016 }
1017 
1018 } // end anon namespace
1019 
1020 void play_sound(const std::string& files, channel_group group, unsigned int repeats)
1021 {
1022  if(prefs::get().sound()) {
1023  play_sound_internal(files, group, repeats);
1024  }
1025 }
1026 
1027 void play_sound_positioned(const std::string& files, int id, int repeats, unsigned int distance)
1028 {
1029  if(prefs::get().sound()) {
1030  play_sound_internal(files, SOUND_SOURCES, repeats, distance, id);
1031  }
1032 }
1033 
1034 // Play bell with separate volume setting
1035 void play_bell(const std::string& files)
1036 {
1037  if(prefs::get().turn_bell()) {
1038  play_sound_internal(files, SOUND_BELL);
1039  }
1040 }
1041 
1042 // Play timer with separate volume setting
1043 void play_timer(const std::string& files, const std::chrono::milliseconds& loop_ticks, const std::chrono::milliseconds& fadein_ticks)
1044 {
1045  if(prefs::get().sound()) {
1046  play_sound_internal(files, SOUND_TIMER, 0, 0, -1, loop_ticks, fadein_ticks);
1047  }
1048 }
1049 
1050 // Play UI sounds on separate volume than soundfx
1051 void play_UI_sound(const std::string& files)
1052 {
1053  if(prefs::get().ui_sound_on()) {
1054  play_sound_internal(files, SOUND_UI);
1055  }
1056 }
1057 
1059 {
1060  if(mix_ok) {
1061  return Mix_VolumeMusic(-1);
1062  }
1063 
1064  return 0;
1065 }
1066 
1067 void set_music_volume(int vol)
1068 {
1069  if(mix_ok && vol >= 0) {
1070  if(vol > MIX_MAX_VOLUME) {
1071  vol = MIX_MAX_VOLUME;
1072  }
1073 
1074  Mix_VolumeMusic(vol);
1075  }
1076 }
1077 
1079 {
1080  if(mix_ok) {
1081  // Since set_sound_volume sets all main channels to the same, just return the volume of any main channel
1082  return Mix_Volume(source_channel_start, -1);
1083  }
1084  return 0;
1085 }
1086 
1087 void set_sound_volume(int vol)
1088 {
1089  if(mix_ok && vol >= 0) {
1090  if(vol > MIX_MAX_VOLUME) {
1091  vol = MIX_MAX_VOLUME;
1092  }
1093 
1094  // Bell, timer and UI have separate channels which we can't set up from this
1095  for(unsigned i = 0; i < n_of_channels; ++i) {
1096  if(!(i >= UI_sound_channel_start && i <= UI_sound_channel_last) && i != bell_channel
1097  && i != timer_channel) {
1098  Mix_Volume(i, vol);
1099  }
1100  }
1101  }
1102 }
1103 
1104 /*
1105  * For the purpose of volume setting, we treat turn timer the same as bell
1106  */
1107 void set_bell_volume(int vol)
1108 {
1109  if(mix_ok && vol >= 0) {
1110  if(vol > MIX_MAX_VOLUME) {
1111  vol = MIX_MAX_VOLUME;
1112  }
1113 
1114  Mix_Volume(bell_channel, vol);
1115  Mix_Volume(timer_channel, vol);
1116  }
1117 }
1118 
1119 void set_UI_volume(int vol)
1120 {
1121  if(mix_ok && vol >= 0) {
1122  if(vol > MIX_MAX_VOLUME) {
1123  vol = MIX_MAX_VOLUME;
1124  }
1125 
1126  for(unsigned i = UI_sound_channel_start; i <= UI_sound_channel_last; ++i) {
1127  Mix_Volume(i, vol);
1128  }
1129  }
1130 }
1131 
1132 } // end namespace sound
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
virtual void join_global()
Definition: events.cpp:371
bool turn_bell()
static prefs & get()
bool sound()
std::size_t sound_buffer_size()
bool music_on()
bool ui_sound_on()
static rng & default_instance()
Definition: random.cpp:73
int get_random_int(int min, int max)
Definition: random.hpp:51
void handle_window_event(const SDL_Event &event) override
Definition: sound.cpp:804
Internal representation of music tracks.
const std::string & file_path() const
static std::shared_ptr< music_track > create(const config &cfg)
bool operator==(const config &a, const config &b)
Definition: config.cpp:1352
bool operator!=(const config &a, const config &b)
Definition: config.hpp:153
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1032
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
Standard logging facilities (interface).
static lua_music_track * get_track(lua_State *L, int i)
Definition: lua_audio.cpp:65
Handling of system events.
std::unique_ptr< SDL_RWops, sdl_rwops_deleter > rwops_ptr
Definition: filesystem.hpp:61
rwops_ptr make_read_RWops(const std::string &path)
utils::optional< std::string > get_binary_file_location(const std::string &type, const std::string &filename)
Returns a complete path to the actual file of a given type, if it exists.
utils::optional< std::string > get_localized_path(const std::string &file, const std::string &suff)
Returns the localized version of the given filename, if it exists.
std::string turn_bell
static int bell_volume()
static int music_volume()
static bool sound()
static int ui_volume()
static bool ui_sound_on()
static int sound_volume()
static bool music_on()
std::vector< game_tip > shuffle(const std::vector< game_tip > &tips)
Shuffles the tips.
Definition: tips.cpp:43
Audio output for sound and music.
Definition: sound.cpp:39
void write_music_play_list(config &snapshot)
Definition: sound.cpp:846
void empty_playlist()
Definition: sound.cpp:611
int get_music_volume()
Definition: sound.cpp:1058
void set_bell_volume(int vol)
Definition: sound.cpp:1107
void play_sound(const std::string &files, channel_group group, unsigned int repeats)
Definition: sound.cpp:1020
void reset_sound()
Definition: sound.cpp:523
bool init_sound()
Definition: sound.cpp:440
void close_sound()
Definition: sound.cpp:492
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:689
void remove_track(unsigned int i)
Definition: sound.cpp:243
void play_music()
Definition: sound.cpp:616
unsigned int get_num_tracks()
Definition: sound.cpp:218
void stop_music()
Definition: sound.cpp:554
void play_music_once(const std::string &file)
Definition: sound.cpp:600
void play_sound_positioned(const std::string &files, int id, int repeats, unsigned int distance)
Definition: sound.cpp:1027
utils::optional< unsigned int > get_current_track_index()
Definition: sound.cpp:198
void set_track(unsigned int i, const std::shared_ptr< music_track > &to)
Definition: sound.cpp:236
void stop_UI_sound()
Definition: sound.cpp:589
std::vector< std::string > enumerate_drivers()
Definition: sound.cpp:415
void flush_cache()
Definition: sound.cpp:192
bool is_sound_playing(int id)
Definition: sound.cpp:872
void stop_sound(int id)
Definition: sound.cpp:878
void stop_bell()
Definition: sound.cpp:577
static std::vector< Mix_Chunk * > channel_chunks
Definition: sound.cpp:41
static std::vector< int > channel_ids
Definition: sound.cpp:45
void reposition_sound(int id, unsigned int distance)
Definition: sound.cpp:856
void play_timer(const std::string &files, const std::chrono::milliseconds &loop_ticks, const std::chrono::milliseconds &fadein_ticks)
Definition: sound.cpp:1043
int get_sound_volume()
Definition: sound.cpp:1078
void commit_music_changes()
Definition: sound.cpp:817
static void channel_finished_hook(int channel)
Definition: sound.cpp:403
static void play_new_music()
Definition: sound.cpp:640
void play_track(unsigned int i)
Definition: sound.cpp:628
void play_UI_sound(const std::string &files)
Definition: sound.cpp:1051
void set_previous_track(std::shared_ptr< music_track > track)
Definition: sound.cpp:213
std::shared_ptr< music_track > get_previous_music_track()
Definition: sound.cpp:209
void set_music_volume(int vol)
Definition: sound.cpp:1067
std::shared_ptr< music_track > get_current_track()
Definition: sound.cpp:205
void stop_sound()
Definition: sound.cpp:562
channel_group
Definition: sound.hpp:28
@ NULL_CHANNEL
Definition: sound.hpp:29
@ SOUND_UI
Definition: sound.hpp:33
@ SOUND_SOURCES
Definition: sound.hpp:30
@ SOUND_TIMER
Definition: sound.hpp:32
@ SOUND_FX
Definition: sound.hpp:34
@ SOUND_BELL
Definition: sound.hpp:31
std::string current_driver()
Definition: sound.cpp:409
void set_UI_volume(int vol)
Definition: sound.cpp:1119
void set_sound_volume(int vol)
Definition: sound.cpp:1087
void play_bell(const std::string &files)
Definition: sound.cpp:1035
void process(int mousex, int mousey)
Definition: tooltips.cpp:334
auto find(Container &container, const Value &value, const Projection &projection={})
Definition: general.hpp:179
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition: general.hpp:87
std::vector< std::string > square_parenthetical_split(const std::string &val, const char separator, const std::string &left, const std::string &right, const int flags)
Similar to parenthetical_split, but also expands embedded square brackets.
auto * find(Container &container, const Value &value)
Convenience wrapper for using find on a container without needing to comare to end()
Definition: general.hpp:141
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
static void decrement_chunk_usage(Mix_Chunk *mcp)
Definition: sound.cpp:88
#define DBG_AUDIO
Definition: sound.cpp:34
static std::shared_ptr< sound::music_track > choose_track()
Definition: sound.cpp:327
static void increment_chunk_usage(Mix_Chunk *mcp)
Definition: sound.cpp:83
static lg::log_domain log_audio("audio")
#define ERR_AUDIO
Definition: sound.cpp:36
#define LOG_AUDIO
Definition: sound.cpp:35
static bool track_ok(const std::string &id)
Definition: sound.cpp:265
static std::string pick_one(const std::string &files)
Definition: sound.cpp:352
#define DISTANCE_SILENT
Definition: sound.hpp:74
std::string filename
Filename.
mock_char c
static map_location::direction n
channel
Definition: utils.hpp:104
#define d
#define f