The Battle for Wesnoth  1.19.11+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/rate_counter.hpp"
24 
25 #include <SDL2/SDL.h>
26 #include <SDL2/SDL_mixer.h>
27 
28 #include <list>
29 #include <string>
30 #include <utility>
31 
32 static lg::log_domain log_audio("audio");
33 #define DBG_AUDIO LOG_STREAM(debug, log_audio)
34 #define LOG_AUDIO LOG_STREAM(info, log_audio)
35 #define ERR_AUDIO LOG_STREAM(err, log_audio)
36 
37 namespace sound
38 {
39 // Channel-chunk mapping lets us know, if we can safely free a given chunk
40 static std::vector<Mix_Chunk*> channel_chunks;
41 
42 // Channel-id mapping for use with sound sources (to check if given source
43 // is playing on a channel for fading/panning)
44 static std::vector<int> channel_ids;
45 }
46 
47 using namespace std::chrono_literals;
48 
49 namespace
50 {
51 bool mix_ok = false;
52 utils::optional<std::chrono::steady_clock::time_point> music_start_time;
53 utils::rate_counter music_refresh_rate{20};
54 bool want_new_music = false;
55 auto fade_out_time = 5000ms;
56 bool no_fading = false;
57 bool unload_music = false;
58 
59 // number of allocated channels,
60 const std::size_t n_of_channels = 32;
61 
62 // we need 2 channels, because we it for timer as well
63 const std::size_t bell_channel = 0;
64 const std::size_t timer_channel = 1;
65 
66 // number of channels reserved for sound sources
67 const std::size_t source_channels = 8;
68 const std::size_t source_channel_start = timer_channel + 1;
69 const std::size_t source_channel_last = source_channel_start + source_channels - 1;
70 const std::size_t UI_sound_channels = 2;
71 const std::size_t UI_sound_channel_start = source_channel_last + 1;
72 const std::size_t UI_sound_channel_last = UI_sound_channel_start + UI_sound_channels - 1;
73 const std::size_t n_reserved_channels = UI_sound_channel_last + 1; // sources, bell, timer and UI
74 
75 // Max number of sound chunks that we want to cache
76 // Keep this above number of available channels to avoid busy-looping
77 unsigned max_cached_chunks = 256;
78 
79 std::map<Mix_Chunk*, int> chunk_usage;
80 } // end anon namespace
81 
82 static void increment_chunk_usage(Mix_Chunk* mcp)
83 {
84  ++(chunk_usage[mcp]);
85 }
86 
87 static void decrement_chunk_usage(Mix_Chunk* mcp)
88 {
89  if(mcp == nullptr) {
90  return;
91  }
92 
93  std::map<Mix_Chunk*, int>::iterator this_usage = chunk_usage.find(mcp);
94  assert(this_usage != chunk_usage.end());
95  if(--(this_usage->second) == 0) {
96  Mix_FreeChunk(mcp);
97  chunk_usage.erase(this_usage);
98  }
99 }
100 
101 namespace
102 {
103 class sound_cache_chunk
104 {
105 public:
106  sound_cache_chunk(const std::string& f)
107  : group(sound::NULL_CHANNEL)
108  , file(f)
109  , data_(nullptr)
110  {
111  }
112  sound_cache_chunk(const sound_cache_chunk& scc)
113  : group(scc.group)
114  , file(scc.file)
115  , data_(scc.data_)
116  {
117  increment_chunk_usage(data_);
118  }
119 
120  ~sound_cache_chunk()
121  {
122  decrement_chunk_usage(data_);
123  }
124 
125  sound::channel_group group;
126  std::string file;
127 
128  void set_data(Mix_Chunk* d)
129  {
131  decrement_chunk_usage(data_);
132  data_ = d;
133  }
134 
135  Mix_Chunk* get_data() const
136  {
137  return data_;
138  }
139 
140  bool operator==(const sound_cache_chunk& scc) const
141  {
142  return file == scc.file;
143  }
144 
145  bool operator!=(const sound_cache_chunk& scc) const
146  {
147  return !operator==(scc);
148  }
149 
150  sound_cache_chunk& operator=(const sound_cache_chunk& scc)
151  {
152  file = scc.file;
153  group = scc.group;
154  set_data(scc.get_data());
155  return *this;
156  }
157 
158 private:
159  Mix_Chunk* data_;
160 };
161 
162 std::list<sound_cache_chunk> sound_cache;
163 typedef std::list<sound_cache_chunk>::iterator sound_cache_iterator;
164 std::map<std::string, std::shared_ptr<Mix_Music>> music_cache;
165 
166 std::vector<std::string> played_before;
167 
168 //
169 // FIXME: the first music_track may be initialized before main()
170 // is reached. Using the logging facilities may lead to a SIGSEGV
171 // because it's not guaranteed that their objects are already alive.
172 //
173 // Use the music_track default constructor to avoid trying to
174 // invoke a log object while resolving paths.
175 //
176 std::vector<std::shared_ptr<sound::music_track>> current_track_list;
177 std::shared_ptr<sound::music_track> current_track;
178 unsigned int current_track_index = 0;
179 std::shared_ptr<sound::music_track> previous_track;
180 
181 std::vector<std::shared_ptr<sound::music_track>>::const_iterator find_track(const sound::music_track& track)
182 {
183  return std::find_if(current_track_list.begin(), current_track_list.end(),
184  [&track](const std::shared_ptr<const sound::music_track>& ptr) { return *ptr == track; }
185  );
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  set_previous_track(current_track);
603  current_track = std::make_shared<music_track>(file);
604  current_track->set_play_once(true);
605  current_track_index = current_track_list.size();
606  play_music();
607 }
608 
610 {
611  current_track_list.clear();
612 }
613 
615 {
616  if(!current_track) {
617  return;
618  }
619 
620  music_start_time = std::chrono::steady_clock::now(); // immediate
621  want_new_music = true;
622  no_fading = false;
623  fade_out_time = previous_track != nullptr ? previous_track->ms_after() : 0ms;
624 }
625 
626 void play_track(unsigned int i)
627 {
628  set_previous_track(current_track);
629  if(i >= current_track_list.size()) {
630  current_track = choose_track();
631  } else {
632  current_track_index = i;
633  current_track = current_track_list[i];
634  }
635  play_music();
636 }
637 
638 static void play_new_music()
639 {
640  music_start_time.reset(); // reset status: no start time
641  want_new_music = true;
642 
643  if(!prefs::get().music_on() || !mix_ok || !current_track || !current_track->valid()) {
644  return;
645  }
646 
647  std::string filename = current_track->file_path();
648  if(auto localized = filesystem::get_localized_path(filename)) {
649  filename = localized.value();
650  }
651 
652  auto itor = music_cache.find(filename);
653  if(itor == music_cache.end()) {
654  LOG_AUDIO << "attempting to insert track '" << filename << "' into cache";
655 
657  // SDL takes ownership of rwops
658  const std::shared_ptr<Mix_Music> music(Mix_LoadMUSType_RW(rwops.release(), MUS_NONE, true), &Mix_FreeMusic);
659 
660  if(music == nullptr) {
661  ERR_AUDIO << "Could not load music file '" << filename << "': " << Mix_GetError();
662  return;
663  }
664 
665  itor = music_cache.emplace(filename, music).first;
666  }
667 
668  LOG_AUDIO << "Playing track '" << filename << "'";
669  auto fading_time = current_track->ms_before();
670  if(no_fading) {
671  fading_time = 0ms;
672  }
673 
674  // Halt any existing music.
675  // If we don't do this SDL_Mixer blocks everything until fade out is complete.
676  // Do not remove this without ensuring that it does not block.
677  // If you don't want it to halt the music, ensure that fades are completed
678  // before attempting to play new music.
679  Mix_HaltMusic();
680 
681  // Fade in the new music
682  const int res = Mix_FadeInMusic(itor->second.get(), 1, fading_time.count());
683  if(res < 0) {
684  ERR_AUDIO << "Could not play music: " << Mix_GetError() << " " << filename << " ";
685  }
686 
687  want_new_music = false;
688 }
689 
690 void play_music_repeatedly(const std::string& id)
691 {
692  // Can happen if scenario doesn't specify.
693  if(id.empty()) {
694  return;
695  }
696 
697  current_track_list.clear();
698  current_track_list.emplace_back(new music_track(id));
699 
700  std::shared_ptr<music_track> last_track = current_track;
701  current_track = current_track_list.back();
702  current_track_index = 0;
703 
704  // If we're already playing it, don't interrupt.
705  if(!last_track || !current_track || *last_track != *current_track) {
706  play_music();
707  }
708 
709  last_track.reset();
710 }
711 
712 void play_music_config(const config& music_node, bool allow_interrupt_current_track, int i)
713 {
714  //
715  // FIXME: there is a memory leak somewhere in this function, seemingly related to the shared_ptrs
716  // stored in current_track_list.
717  //
718  // vultraz 5/8/2017
719  //
720 
721  music_track track(music_node);
722 
723  if(!track.valid() && !track.id().empty()) {
724  ERR_AUDIO << "cannot open track '" << track.id() << "'; disabled in this playlist.";
725  }
726 
727  // If they say play once, we don't alter playlist.
728  if(track.play_once()) {
729  set_previous_track(current_track);
730  current_track = std::make_shared<music_track>(track);
731  current_track_index = current_track_list.size();
732  play_music();
733  return;
734  }
735 
736  // Clear play list unless they specify append.
737  if(!track.append()) {
738  current_track_list.clear();
739  }
740 
741  if(!track.valid()) {
742  return;
743  }
744 
745  auto iter = find_track(track);
746  // Avoid 2 tracks with the same name, since that can cause an infinite loop
747  // in choose_track(), 2 tracks with the same name will always return the
748  // current track and track_ok() doesn't allow that.
749  if(iter == current_track_list.end()) {
750  if(i < 0 || static_cast<std::size_t>(i) >= current_track_list.size()) {
751  current_track_list.emplace_back(new music_track(track));
752  iter = current_track_list.end() - 1;
753  } else {
754  iter = current_track_list.emplace(current_track_list.begin() + 1, new music_track(track));
755  if(current_track_index >= static_cast<std::size_t>(i)) {
756  current_track_index++;
757  }
758  }
759  } else {
760  ERR_AUDIO << "tried to add duplicate track '" << track.file_path() << "'";
761  }
762 
763  // They can tell us to start playing this list immediately.
764  if(track.immediate()) {
765  set_previous_track(current_track);
766  current_track = *iter;
767  current_track_index = iter - current_track_list.begin();
768  play_music();
769  } else if(!track.append() && !allow_interrupt_current_track && current_track) {
770  // Make sure the current track will finish first
771  current_track->set_play_once(true);
772  }
773 }
774 
776 {
777  if(Mix_FadingMusic() != MIX_NO_FADING) {
778  // Do not block everything while fading.
779  return;
780  }
781 
782  if(prefs::get().music_on()) {
783  // TODO: rethink the music_thinker design, especially the use of fade_out_time
784  auto now = std::chrono::steady_clock::now();
785 
786  if(!music_start_time && !current_track_list.empty() && !Mix_PlayingMusic()) {
787  // Pick next track, add ending time to its start time.
788  set_previous_track(current_track);
789  current_track = choose_track();
790  music_start_time = now;
791  no_fading = true;
792  fade_out_time = 0ms;
793  }
794 
795  if(music_start_time && music_refresh_rate.poll()) {
796  want_new_music = now >= *music_start_time - fade_out_time;
797  }
798 
799  if(want_new_music) {
800  if(Mix_PlayingMusic()) {
801  Mix_FadeOutMusic(fade_out_time.count());
802  return;
803  }
804 
805  unload_music = false;
806  play_new_music();
807  }
808  }
809 
810  if(unload_music) {
811  // The custom shared_ptr deleter (Mix_FreeMusic) will handle the freeing of each track.
812  music_cache.clear();
813 
814  Mix_HookMusicFinished(nullptr);
815 
816  unload_music = false;
817  }
818 }
819 
820 music_muter::music_muter()
821  : events::sdl_handler(false)
822 {
823  join_global();
824 }
825 
826 void music_muter::handle_window_event(const SDL_Event& event)
827 {
828  if(prefs::get().stop_music_in_background() && prefs::get().music_on()) {
829  if(event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
830  Mix_ResumeMusic();
831  } else if(event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
832  if(Mix_PlayingMusic()) {
833  Mix_PauseMusic();
834  }
835  }
836  }
837 }
838 
840 {
841  played_before.clear();
842 
843  // Play-once is OK if still playing.
844  if(current_track) {
845  if(current_track->play_once()) {
846  return;
847  }
848 
849  // If current track no longer on playlist, change it.
850  for(auto m : current_track_list) {
851  if(*current_track == *m) {
852  return;
853  }
854  }
855  }
856 
857  // Victory empties playlist: if next scenario doesn't specify one...
858  if(current_track_list.empty()) {
859  return;
860  }
861 
862  // FIXME: we don't pause ms_before on this first track. Should we?
863  set_previous_track(current_track);
864  current_track = choose_track();
865  play_music();
866 }
867 
869 {
870  // First entry clears playlist, others append to it.
871  bool append = false;
872  for(auto m : current_track_list) {
873  m->write(snapshot, append);
874  append = true;
875  }
876 }
877 
878 void reposition_sound(int id, unsigned int distance)
879 {
880  audio_lock lock;
881  for(unsigned ch = 0; ch < channel_ids.size(); ++ch) {
882  if(channel_ids[ch] != id) {
883  continue;
884  }
885 
886  if(distance >= DISTANCE_SILENT) {
887  Mix_HaltChannel(ch);
888  } else {
889  Mix_SetDistance(ch, distance);
890  }
891  }
892 }
893 
894 bool is_sound_playing(int id)
895 {
896  audio_lock lock;
897  return std::find(channel_ids.begin(), channel_ids.end(), id) != channel_ids.end();
898 }
899 
900 void stop_sound(int id)
901 {
903 }
904 
905 namespace
906 {
907 struct chunk_load_exception
908 {
909 };
910 
911 Mix_Chunk* load_chunk(const std::string& file, channel_group group)
912 {
913  sound_cache_iterator it_bgn, it_end;
914  sound_cache_iterator it;
915 
916  sound_cache_chunk temp_chunk(file); // search the sound cache on this key
917  it_bgn = sound_cache.begin();
918  it_end = sound_cache.end();
919  it = std::find(it_bgn, it_end, temp_chunk);
920 
921  if(it != it_end) {
922  if(it->group != group) {
923  // cached item has been used in multiple sound groups
924  it->group = NULL_CHANNEL;
925  }
926 
927  // splice the most recently used chunk to the front of the cache
928  sound_cache.splice(it_bgn, sound_cache, it);
929  } else {
930  // remove the least recently used chunk from cache if it's full
931  bool cache_full = (sound_cache.size() == max_cached_chunks);
932  while(cache_full && it != it_bgn) {
933  // make sure this chunk is not being played before freeing it
935  if(std::find(channel_chunks.begin(), ch_end, (--it)->get_data()) == ch_end) {
936  sound_cache.erase(it);
937  cache_full = false;
938  }
939  }
940 
941  if(cache_full) {
942  LOG_AUDIO << "Maximum sound cache size reached and all are busy, skipping.";
943  throw chunk_load_exception();
944  }
945 
946  temp_chunk.group = group;
947  const auto filename = filesystem::get_binary_file_location("sounds", file);
948  const auto localized = filesystem::get_localized_path(filename.value_or(""));
949 
950  if(filename) {
951  filesystem::rwops_ptr rwops = filesystem::make_read_RWops(localized.value_or(filename.value()));
952  temp_chunk.set_data(Mix_LoadWAV_RW(rwops.release(), true)); // SDL takes ownership of rwops
953  } else {
954  ERR_AUDIO << "Could not load sound file '" << file << "'.";
955  throw chunk_load_exception();
956  }
957 
958  if(temp_chunk.get_data() == nullptr) {
959  ERR_AUDIO << "Could not load sound file '" << filename.value() << "': " << Mix_GetError();
960  throw chunk_load_exception();
961  }
962 
963  sound_cache.push_front(temp_chunk);
964  }
965 
966  return sound_cache.begin()->get_data();
967 }
968 
969 using namespace std::chrono_literals;
970 
971 void play_sound_internal(const std::string& files,
972  channel_group group,
973  unsigned int repeats = 0,
974  unsigned int distance = 0,
975  int id = -1,
976  const std::chrono::milliseconds& loop_ticks = 0ms,
977  const std::chrono::milliseconds& fadein_ticks = 0ms)
978 {
979  if(files.empty() || distance >= DISTANCE_SILENT || !mix_ok) {
980  return;
981  }
982 
983  audio_lock lock;
984 
985  // find a free channel in the desired group
986  int channel = Mix_GroupAvailable(group);
987  if(channel == -1) {
988  LOG_AUDIO << "All channels dedicated to sound group(" << group << ") are busy, skipping.";
989  return;
990  }
991 
992  Mix_Chunk* chunk;
993  std::string file = pick_one(files);
994 
995  try {
996  chunk = load_chunk(file, group);
997  assert(chunk);
998  } catch(const chunk_load_exception&) {
999  return;
1000  }
1001 
1002  /*
1003  * This check prevents SDL_Mixer from blowing up on Windows when UI sound is played
1004  * in response to toggling the checkbox which disables sound.
1005  */
1006  if(group != SOUND_UI) {
1007  Mix_SetDistance(channel, distance);
1008  }
1009 
1010  int res;
1011  if(loop_ticks > 0ms) {
1012  if(fadein_ticks > 0ms) {
1013  res = Mix_FadeInChannelTimed(channel, chunk, -1, fadein_ticks.count(), loop_ticks.count());
1014  } else {
1015  res = Mix_PlayChannel(channel, chunk, -1);
1016  }
1017 
1018  if(res >= 0) {
1019  Mix_ExpireChannel(channel, loop_ticks.count());
1020  }
1021  } else {
1022  if(fadein_ticks > 0ms) {
1023  res = Mix_FadeInChannel(channel, chunk, repeats, fadein_ticks.count());
1024  } else {
1025  res = Mix_PlayChannel(channel, chunk, repeats);
1026  }
1027  }
1028 
1029  if(res < 0) {
1030  ERR_AUDIO << "error playing sound effect: " << Mix_GetError();
1031  // still keep it in the sound cache, in case we want to try again later
1032  return;
1033  }
1034 
1035  channel_ids[channel] = id;
1036 
1037  // reserve the channel's chunk from being freed, since it is playing
1038  channel_chunks[res] = chunk;
1039 }
1040 
1041 } // end anon namespace
1042 
1043 void play_sound(const std::string& files, channel_group group, unsigned int repeats)
1044 {
1045  if(prefs::get().sound()) {
1046  play_sound_internal(files, group, repeats);
1047  }
1048 }
1049 
1050 void play_sound_positioned(const std::string& files, int id, int repeats, unsigned int distance)
1051 {
1052  if(prefs::get().sound()) {
1053  play_sound_internal(files, SOUND_SOURCES, repeats, distance, id);
1054  }
1055 }
1056 
1057 // Play bell with separate volume setting
1058 void play_bell(const std::string& files)
1059 {
1060  if(prefs::get().turn_bell()) {
1061  play_sound_internal(files, SOUND_BELL);
1062  }
1063 }
1064 
1065 // Play timer with separate volume setting
1066 void play_timer(const std::string& files, const std::chrono::milliseconds& loop_ticks, const std::chrono::milliseconds& fadein_ticks)
1067 {
1068  if(prefs::get().sound()) {
1069  play_sound_internal(files, SOUND_TIMER, 0, 0, -1, loop_ticks, fadein_ticks);
1070  }
1071 }
1072 
1073 // Play UI sounds on separate volume than soundfx
1074 void play_UI_sound(const std::string& files)
1075 {
1076  if(prefs::get().ui_sound_on()) {
1077  play_sound_internal(files, SOUND_UI);
1078  }
1079 }
1080 
1082 {
1083  if(mix_ok) {
1084  return Mix_VolumeMusic(-1);
1085  }
1086 
1087  return 0;
1088 }
1089 
1090 void set_music_volume(int vol)
1091 {
1092  if(mix_ok && vol >= 0) {
1093  if(vol > MIX_MAX_VOLUME) {
1094  vol = MIX_MAX_VOLUME;
1095  }
1096 
1097  Mix_VolumeMusic(vol);
1098  }
1099 }
1100 
1102 {
1103  if(mix_ok) {
1104  // Since set_sound_volume sets all main channels to the same, just return the volume of any main channel
1105  return Mix_Volume(source_channel_start, -1);
1106  }
1107  return 0;
1108 }
1109 
1110 void set_sound_volume(int vol)
1111 {
1112  if(mix_ok && vol >= 0) {
1113  if(vol > MIX_MAX_VOLUME) {
1114  vol = MIX_MAX_VOLUME;
1115  }
1116 
1117  // Bell, timer and UI have separate channels which we can't set up from this
1118  for(unsigned i = 0; i < n_of_channels; ++i) {
1119  if(!(i >= UI_sound_channel_start && i <= UI_sound_channel_last) && i != bell_channel
1120  && i != timer_channel) {
1121  Mix_Volume(i, vol);
1122  }
1123  }
1124  }
1125 }
1126 
1127 /*
1128  * For the purpose of volume setting, we treat turn timer the same as bell
1129  */
1130 void set_bell_volume(int vol)
1131 {
1132  if(mix_ok && vol >= 0) {
1133  if(vol > MIX_MAX_VOLUME) {
1134  vol = MIX_MAX_VOLUME;
1135  }
1136 
1137  Mix_Volume(bell_channel, vol);
1138  Mix_Volume(timer_channel, vol);
1139  }
1140 }
1141 
1142 void set_UI_volume(int vol)
1143 {
1144  if(mix_ok && vol >= 0) {
1145  if(vol > MIX_MAX_VOLUME) {
1146  vol = MIX_MAX_VOLUME;
1147  }
1148 
1149  for(unsigned i = UI_sound_channel_start; i <= UI_sound_channel_last; ++i) {
1150  Mix_Volume(i, vol);
1151  }
1152  }
1153 }
1154 
1155 } // 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:374
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:826
Internal representation of music tracks.
const std::string & file_path() const
const std::string & id() const
bool operator==(const config &a, const config &b)
Definition: config.cpp:1358
bool operator!=(const config &a, const config &b)
Definition: config.hpp:153
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1030
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:70
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:38
void write_music_play_list(config &snapshot)
Definition: sound.cpp:868
void empty_playlist()
Definition: sound.cpp:609
int get_music_volume()
Definition: sound.cpp:1081
void set_bell_volume(int vol)
Definition: sound.cpp:1130
void play_sound(const std::string &files, channel_group group, unsigned int repeats)
Definition: sound.cpp:1043
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:712
void remove_track(unsigned int i)
Definition: sound.cpp:243
void play_music_repeatedly(const std::string &id)
Definition: sound.cpp:690
void play_music()
Definition: sound.cpp:614
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:1050
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:894
void stop_sound(int id)
Definition: sound.cpp:900
void stop_bell()
Definition: sound.cpp:577
static std::vector< Mix_Chunk * > channel_chunks
Definition: sound.cpp:40
static std::vector< int > channel_ids
Definition: sound.cpp:44
void reposition_sound(int id, unsigned int distance)
Definition: sound.cpp:878
void play_timer(const std::string &files, const std::chrono::milliseconds &loop_ticks, const std::chrono::milliseconds &fadein_ticks)
Definition: sound.cpp:1066
int get_sound_volume()
Definition: sound.cpp:1101
void commit_music_changes()
Definition: sound.cpp:839
static void channel_finished_hook(int channel)
Definition: sound.cpp:403
static void play_new_music()
Definition: sound.cpp:638
void play_track(unsigned int i)
Definition: sound.cpp:626
void play_UI_sound(const std::string &files)
Definition: sound.cpp:1074
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:1090
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:1142
void set_sound_volume(int vol)
Definition: sound.cpp:1110
void play_bell(const std::string &files)
Definition: sound.cpp:1058
void process(int mousex, int mousey)
Definition: tooltips.cpp:340
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:140
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
static void decrement_chunk_usage(Mix_Chunk *mcp)
Definition: sound.cpp:87
#define DBG_AUDIO
Definition: sound.cpp:33
static std::shared_ptr< sound::music_track > choose_track()
Definition: sound.cpp:327
static void increment_chunk_usage(Mix_Chunk *mcp)
Definition: sound.cpp:82
static lg::log_domain log_audio("audio")
#define ERR_AUDIO
Definition: sound.cpp:35
#define LOG_AUDIO
Definition: sound.cpp:34
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:76
std::string filename
Filename.
mock_char c
static map_location::direction n
channel
Definition: utils.hpp:104
#define d
#define f