The Battle for Wesnoth  1.19.25+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"
23 #include "sound_music_track.hpp"
24 #include "utils/general.hpp"
25 #include "utils/rate_counter.hpp"
26 
27 #include <SDL3_mixer/SDL_mixer.h>
28 
29 #include <list>
30 #include <mutex>
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 namespace
41 {
42 /** Lightweight lifetime wrapper for MIX_Track. */
43 class track_source
44 {
45 public:
46  /** Creates a new type-tagged track on @a mixer. */
47  void allocate_track(MIX_Mixer* mixer, const char* type_tag)
48  {
49  track_.reset(MIX_CreateTrack(mixer));
50  MIX_TagTrack(*this, type_tag);
51  }
52 
53  /** Implicit conversion for use with the SDL_Mixer API. */
54  operator MIX_Track*() const
55  {
56  return track_.get();
57  }
58 
59 private:
60  std::unique_ptr<MIX_Track, decltype(&MIX_DestroyTrack)> track_{nullptr, &MIX_DestroyTrack};
61 };
62 
63 std::mutex soundsource_map_mutex;
64 std::map<unsigned int, MIX_Track*> soundsource_map;
65 
66 std::array<track_source, 32> track_pool{};
67 
68 // One of these can play at a time
69 const auto music_tracks = utils::span{track_pool}.subspan<0, 1>();
70 const auto bell_tracks = utils::span{track_pool}.subspan<1, 1>();
71 const auto timer_tracks = utils::span{track_pool}.subspan<2, 1>();
72 
73 // Several of these can play at a time
74 const auto sound_source_tracks = utils::span{track_pool}.subspan<3, 8>();
75 const auto UI_sound_tracks = utils::span{track_pool}.subspan<11, 2>();
76 const auto sound_fx_tracks = utils::span{track_pool}.subspan<13, 19>();
77 
78 // filename, audio
79 std::map<std::string, std::shared_ptr<MIX_Audio>> music_cache;
80 std::vector<std::string> music_cache_insertion_order;
81 
82 std::map<std::string, std::shared_ptr<MIX_Audio>> sound_cache;
83 std::vector<std::string> sound_cache_insertion_order;
84 
85 MIX_Mixer* mixer;
86 std::size_t mixer_init_counter = 0;
87 
88 using namespace std::chrono_literals;
89 
90 bool mix_ok = false;
91 utils::optional<std::chrono::steady_clock::time_point> music_start_time;
92 utils::rate_counter music_refresh_rate{20};
93 bool want_new_music = false;
94 auto fade_out_time = 5000ms;
95 bool no_fading = false;
96 
97 const std::size_t music_cache_limit = 30;
98 const std::size_t sound_cache_limit = 500;
99 
100 std::vector<std::string> played_before;
101 
102 //
103 // FIXME: the first music_track may be initialized before main()
104 // is reached. Using the logging facilities may lead to a SIGSEGV
105 // because it's not guaranteed that their objects are already alive.
106 //
107 // Use the music_track default constructor to avoid trying to
108 // invoke a log object while resolving paths.
109 //
110 std::vector<std::shared_ptr<sound::music_track>> current_track_list;
111 std::shared_ptr<sound::music_track> current_track;
112 unsigned int current_track_index = 0;
113 std::shared_ptr<sound::music_track> previous_track;
114 
115 std::vector<std::shared_ptr<sound::music_track>>::const_iterator find_track(const sound::music_track& track)
116 {
117  return utils::ranges::find(current_track_list, track,
118  [](const std::shared_ptr<const sound::music_track>& ptr) { return *ptr; });
119 }
120 
121 } // end anon namespace
122 
123 utils::optional<unsigned int> get_current_track_index()
124 {
125  if(current_track_index >= current_track_list.size()){
126  return {};
127  }
128  return current_track_index;
129 }
130 std::shared_ptr<music_track> get_current_track()
131 {
132  return current_track;
133 }
134 std::shared_ptr<music_track> get_previous_music_track()
135 {
136  return previous_track;
137 }
138 void set_previous_track(std::shared_ptr<music_track> track)
139 {
140  previous_track = std::move(track);
141 }
142 
143 unsigned int get_num_tracks()
144 {
145  return current_track_list.size();
146 }
147 
148 std::shared_ptr<music_track> get_track(unsigned int i)
149 {
150  if(i < current_track_list.size()) {
151  return current_track_list[i];
152  }
153 
154  if(i == current_track_list.size()) {
155  return current_track;
156  }
157 
158  return nullptr;
159 }
160 
161 void set_track(unsigned int i, const std::shared_ptr<music_track>& to)
162 {
163  if(i < current_track_list.size() && find_track(*to) != current_track_list.end()) {
164  current_track_list[i] = std::make_shared<music_track>(*to);
165  }
166 }
167 
168 void remove_track(unsigned int i)
169 {
170  if(i >= current_track_list.size()) {
171  return;
172  }
173 
174  if(i == current_track_index) {
175  // Let the track finish playing
176  if(current_track){
177  current_track->set_play_once(true);
178  }
179  // Set current index to the new size of the list
180  current_track_index = current_track_list.size() - 1;
181  } else if(i < current_track_index) {
182  current_track_index--;
183  }
184 
185  current_track_list.erase(current_track_list.begin() + i);
186 }
187 
188 namespace
189 {
190 bool track_ok(const std::string& id)
191 {
192  LOG_AUDIO << "Considering " << id;
193 
194  if(!current_track) {
195  return true;
196  }
197 
198  // If they committed changes to list, we forget previous plays, but
199  // still *never* repeat same track twice if we have an option.
200  if(id == current_track->file_path()) {
201  return false;
202  }
203 
204  if(current_track_list.size() <= 3) {
205  return true;
206  }
207 
208  // Timothy Pinkham says:
209  // 1) can't be repeated without 2 other pieces have already played
210  // since A was played.
211  // 2) cannot play more than 2 times without every other piece
212  // having played at least 1 time.
213 
214  // Dammit, if our musicians keep coming up with algorithms, I'll
215  // be out of a job!
216  unsigned int num_played = 0;
217  std::set<std::string> played;
218  std::vector<std::string>::reverse_iterator i;
219 
220  for(i = played_before.rbegin(); i != played_before.rend(); ++i) {
221  if(*i == id) {
222  ++num_played;
223  if(num_played == 2) {
224  break;
225  }
226  } else {
227  played.insert(*i);
228  }
229  }
230 
231  // If we've played this twice, must have played every other track.
232  if(num_played == 2 && played.size() != current_track_list.size() - 1) {
233  LOG_AUDIO << "Played twice with only " << played.size() << " tracks between";
234  return false;
235  }
236 
237  // Check previous previous track not same.
238  i = played_before.rbegin();
239  if(i != played_before.rend()) {
240  ++i;
241  if(i != played_before.rend()) {
242  if(*i == id) {
243  LOG_AUDIO << "Played just before previous";
244  return false;
245  }
246  }
247  }
248 
249  return true;
250 }
251 
252 std::shared_ptr<sound::music_track> choose_track()
253 {
254  assert(!current_track_list.empty());
255 
256  if(current_track_index >= current_track_list.size()) {
257  current_track_index = 0;
258  }
259 
260  if(current_track_list[current_track_index]->shuffle()) {
261  unsigned int track = 0;
262 
263  if(current_track_list.size() > 1) {
264  do {
265  track = randomness::rng::default_instance().get_random_int(0, current_track_list.size()-1);
266  } while(!track_ok(current_track_list[track]->file_path()));
267  }
268 
269  current_track_index = track;
270  }
271 
272  DBG_AUDIO << "Next track will be " << current_track_list[current_track_index]->file_path();
273  played_before.push_back(current_track_list[current_track_index]->file_path());
274  return current_track_list[current_track_index];
275 }
276 
277 std::string pick_one(const std::string& files)
278 {
279  std::vector<std::string> ids = utils::square_parenthetical_split(files, ',', "[", "]");
280 
281  if(ids.empty()) {
282  return "";
283  }
284 
285  if(ids.size() == 1) {
286  return ids[0];
287  }
288 
289  // We avoid returning same choice twice if we can avoid it.
290  static std::map<std::string, unsigned int> prev_choices;
291  unsigned int choice;
292 
293  if(prev_choices.find(files) != prev_choices.end()) {
294  choice = randomness::rng::default_instance().get_random_int(0, ids.size()-1 - 1);
295  if(choice >= prev_choices[files]) {
296  ++choice;
297  }
298 
299  prev_choices[files] = choice;
300  } else {
301  choice = randomness::rng::default_instance().get_random_int(0, ids.size()-1);
302  prev_choices.emplace(files, choice);
303  }
304 
305  return ids[choice];
306 }
307 
308 } // namespace
309 
310 std::string current_driver()
311 {
312  const char* const drvname = SDL_GetCurrentAudioDriver();
313  return drvname ? drvname : "<not initialized>";
314 }
315 
316 std::vector<std::string> enumerate_drivers()
317 {
318  std::vector<std::string> res;
319  int num_drivers = SDL_GetNumVideoDrivers();
320 
321  for(int n = 0; n < num_drivers; ++n) {
322  const char* drvname = SDL_GetAudioDriver(n);
323  res.emplace_back(drvname ? drvname : "<invalid driver>");
324  }
325 
326  return res;
327 }
328 
330 {
331  driver_status res{mix_ok, 0, SDL_AUDIO_UNKNOWN, 0, 0};
332 
333  if(mix_ok) {
334  SDL_AudioSpec spec;
335  if(MIX_GetMixerFormat(mixer, &spec)) {
337  res.frequency = spec.freq;
338  res.format = spec.format;
339  res.channels = spec.channels;
340  }
341  }
342 
343  return res;
344 }
345 
347 {
348  LOG_AUDIO << "Initializing audio...";
349  if(SDL_WasInit(SDL_INIT_AUDIO) == 0) {
350  if(!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
351  ERR_AUDIO << "Could not initialize audio: " << SDL_GetError();
352  return false;
353  }
354  }
355 
356  if(MIX_Init()) {
357  mixer_init_counter++;
358  } else {
359  ERR_AUDIO << "Could not initialize mixer: " << SDL_GetError();
360  return false;
361  }
362 
363  if(!mix_ok) {
364  SDL_AudioSpec spec;
365  spec.freq = prefs::get().sample_rate();
366  spec.format = SDL_AUDIO_S16;
367  spec.channels = 2;
368  mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec);
369  if(!mixer) {
370  mix_ok = false;
371  ERR_AUDIO << "Could not initialize audio: " << SDL_GetError();
372  return false;
373  }
374 
375  mix_ok = true;
376 
377  for(track_source& t : music_tracks) {
378  t.allocate_track(mixer, sound_tracks::music);
379  }
380 
381  for(track_source& t : bell_tracks) {
382  t.allocate_track(mixer, sound_tracks::sound_bell);
383  }
384 
385  for(track_source& t : timer_tracks) {
386  t.allocate_track(mixer, sound_tracks::sound_timer);
387  }
388 
389  for(track_source& t : sound_source_tracks) {
390  t.allocate_track(mixer, sound_tracks::sound_source);
391  }
392 
393  for(track_source& t : UI_sound_tracks) {
394  t.allocate_track(mixer, sound_tracks::sound_ui);
395  }
396 
397  for(track_source& t : sound_fx_tracks) {
398  t.allocate_track(mixer, sound_tracks::sound_fx);
399  }
400 
401  set_sound_volume(prefs::get().sound_volume());
402  set_UI_volume(prefs::get().ui_volume());
403  set_music_volume(prefs::get().music_volume());
404  set_bell_volume(prefs::get().bell_volume());
405 
406  LOG_AUDIO << "Audio initialized.";
407  play_music();
408  }
409 
410  return true;
411 }
412 
414 {
415  if(mix_ok) {
416  stop_bell();
417  stop_UI_sound();
418  stop_sound();
419  stop_music();
420  mix_ok = false;
421  }
422 
423  track_pool = {};
424  if(mixer) {
425  MIX_DestroyMixer(mixer);
426  }
427 
428  // as per documentation, calling MIX_Init multiple times won't result in a failure
429  // MIX_Quit then needs to be called the same number of times to make it de-initialize
430  // so, make sure that always happens
431  for(std::size_t i = 0; i < mixer_init_counter; i++) {
432  MIX_Quit();
433  }
434  mixer_init_counter = 0;
435 
436  if(SDL_WasInit(SDL_INIT_AUDIO) != 0) {
437  SDL_QuitSubSystem(SDL_INIT_AUDIO);
438  }
439 
440  LOG_AUDIO << "Audio device released.";
441 }
442 
444 {
445  bool music = prefs::get().music_on();
446  bool sound = prefs::get().sound();
447  bool UI_sound = prefs::get().ui_sound_on();
448  bool bell = prefs::get().turn_bell();
449 
450  if(music || sound || bell || UI_sound) {
453 
454  if(!music) {
456  }
457 
458  if(!sound) {
460  }
461 
462  if(!UI_sound) {
464  }
465 
466  if(!bell) {
468  }
469  }
470 }
471 
473 {
474  if(mix_ok) {
475  for(track_source& t : music_tracks) {
476  MIX_StopTrack(t, MIX_TrackMSToFrames(t, 500));
477  }
478  }
479 }
480 
482 {
483  if(mix_ok) {
484  MIX_StopTag(mixer, sound_tracks::sound_source, 0);
485  MIX_StopTag(mixer, sound_tracks::sound_fx, 0);
486  }
487 }
488 
489 /*
490  * For the purpose of track manipulation, we treat turn timer the same as bell
491  */
492 void stop_bell()
493 {
494  if(mix_ok) {
495  MIX_StopTag(mixer, sound_tracks::sound_bell, 0);
496  MIX_StopTag(mixer, sound_tracks::sound_timer, 0);
497  }
498 }
499 
501 {
502  if(mix_ok) {
503  MIX_StopTag(mixer, sound_tracks::sound_ui, 0);
504  }
505 }
506 
507 void play_music_once(const std::string& file)
508 {
509  if(auto track = sound::music_track::create(file)) {
510  set_previous_track(current_track);
511  current_track = std::move(track);
512  current_track->set_play_once(true);
513  current_track_index = current_track_list.size();
514  play_music();
515  }
516 }
517 
519 {
520  current_track_list.clear();
521 }
522 
524 {
525  if(!current_track) {
526  return;
527  }
528 
529  music_start_time = std::chrono::steady_clock::now(); // immediate
530  want_new_music = true;
531  no_fading = false;
532  fade_out_time = previous_track != nullptr ? previous_track->ms_after() : 0ms;
533 }
534 
535 void play_track(unsigned int i)
536 {
537  set_previous_track(current_track);
538  if(i >= current_track_list.size()) {
539  current_track = choose_track();
540  } else {
541  current_track_index = i;
542  current_track = current_track_list[i];
543  }
544  play_music();
545 }
546 
547 namespace
548 {
549 void play_new_music()
550 {
551  music_start_time.reset(); // reset status: no start time
552  want_new_music = true;
553 
554  if(!prefs::get().music_on() || !mix_ok || !current_track) {
555  return;
556  }
557 
558  std::string filename = current_track->file_path();
559  if(auto localized = filesystem::get_localized_path(filename)) {
560  filename = localized.value();
561  }
562 
563  LOG_AUDIO << "Playing track '" << filename << "'";
564  auto fading_time = current_track->ms_before();
565  if(no_fading) {
566  fading_time = 0ms;
567  }
568 
569  // Halt any existing music.
570  // If we don't do this SDL_Mixer blocks everything until fade out is complete.
571  // Do not remove this without ensuring that it does not block.
572  // If you don't want it to halt the music, ensure that fades are completed
573  // before attempting to play new music.
574  MIX_StopTrack(music_tracks[0], 0);
575 
576  std::shared_ptr<MIX_Audio> music;
577  if(music_cache.count(filename) != 0) {
578  music = music_cache[filename];
579  DBG_AUDIO << "cache hit for " << filename;
580  } else {
581  music.reset(MIX_LoadAudio(mixer, filename.c_str(), false), &MIX_DestroyAudio);
582  DBG_AUDIO << "cache miss for " << filename;
583  }
584 
585  // Fade in the new music
586  MIX_SetTrackAudio(music_tracks[0], music.get());
587 
588  sdl3_properties props;
589  SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fading_time.count());
590 
591  if(!MIX_PlayTrack(music_tracks[0], props)) {
592  ERR_AUDIO << "Could not play music: " << SDL_GetError() << " " << filename << " ";
593  } else if(music_cache.count(filename) == 0) {
594  music_cache.emplace(filename, music);
595  music_cache_insertion_order.emplace_back(filename);
596 
597  if(music_cache.size() > music_cache_limit) {
598  std::string to_erase = music_cache_insertion_order[0];
599  DBG_AUDIO << "Uncaching music file " << to_erase;
600  music_cache_insertion_order.erase(music_cache_insertion_order.begin());
601  music_cache.erase(to_erase);
602  }
603  }
604 
605  want_new_music = false;
606 }
607 
608 } // namespace
609 
610 void play_music_config(const config& music_node, bool allow_interrupt_current_track, int i)
611 {
612  //
613  // FIXME: there is a memory leak somewhere in this function, seemingly related to the shared_ptrs
614  // stored in current_track_list.
615  //
616  // vultraz 5/8/2017
617  //
618 
619  auto track = sound::music_track::create(music_node);
620  if(!track) {
621  ERR_AUDIO << "cannot open track; disabled in this playlist.";
622  return;
623  }
624 
625  // If they say play once, we don't alter playlist.
626  if(track->play_once()) {
627  set_previous_track(current_track);
628  current_track = std::move(track);
629  current_track_index = current_track_list.size();
630  play_music();
631  return;
632  }
633 
634  // Clear play list unless they specify append.
635  if(!track->append()) {
636  current_track_list.clear();
637  }
638 
639  auto iter = find_track(*track);
640  // Avoid 2 tracks with the same name, since that can cause an infinite loop
641  // in choose_track(), 2 tracks with the same name will always return the
642  // current track and track_ok() doesn't allow that.
643  if(iter == current_track_list.end()) {
644  auto insert_at = (i >= 0 && static_cast<std::size_t>(i) < current_track_list.size())
645  ? current_track_list.begin() + i
646  : current_track_list.end();
647 
648  // Copy the track pointer so our local variable remains non-null.
649  iter = current_track_list.insert(insert_at, track);
650  auto new_track_index = std::distance(current_track_list.cbegin(), iter);
651 
652  // If we inserted the new track *before* the current track, adjust
653  // cached index so it still points to the same element.
654  if(new_track_index <= current_track_index) {
655  ++current_track_index;
656  }
657  } else {
658  ERR_AUDIO << "tried to add duplicate track '" << track->file_path() << "'";
659  }
660 
661  // They can tell us to start playing this list immediately.
662  if(track->immediate()) {
663  set_previous_track(current_track);
664  current_track = *iter;
665  current_track_index = std::distance(current_track_list.cbegin(), iter);
666  play_music();
667  } else if(!track->append() && !allow_interrupt_current_track && current_track) {
668  // Make sure the current track will finish first
669  current_track->set_play_once(true);
670  }
671 }
672 
674 {
675  if(prefs::get().music_on()) {
676  // TODO: rethink the music_thinker design, especially the use of fade_out_time
677  auto now = std::chrono::steady_clock::now();
678 
679  bool is_playing = MIX_TrackPlaying(music_tracks[0]);
680  bool is_paused = MIX_TrackPaused(music_tracks[0]);
681  if(!music_start_time && !current_track_list.empty() && !is_playing && !is_paused) {
682  // Pick next track, add ending time to its start time.
683  set_previous_track(current_track);
684  current_track = choose_track();
685  music_start_time = now;
686  no_fading = true;
687  fade_out_time = 0ms;
688  }
689 
690  if(music_start_time && music_refresh_rate.poll()) {
691  want_new_music = now >= *music_start_time - fade_out_time;
692  }
693 
694  if(want_new_music) {
695  if(MIX_TrackPlaying(music_tracks[0])) {
696  MIX_StopTrack(music_tracks[0], MIX_TrackMSToFrames(music_tracks[0], fade_out_time.count()));
697  return;
698  }
699 
700  play_new_music();
701  }
702  }
703 }
704 
706  : events::sdl_handler(false)
707 {
708  join_global();
709 }
710 
711 void music_muter::handle_window_event(const SDL_Event& event)
712 {
713  if(prefs::get().stop_music_in_background() && prefs::get().music_on()) {
714  if(event.type == SDL_EVENT_WINDOW_FOCUS_GAINED) {
715  MIX_ResumeTrack(music_tracks[0]);
716  DBG_AUDIO << "resuming music";
717  } else if(event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
718  if(MIX_TrackPlaying(music_tracks[0])) {
719  MIX_PauseTrack(music_tracks[0]);
720  DBG_AUDIO << "pausing music";
721  }
722  }
723  }
724 }
725 
727 {
728  played_before.clear();
729 
730  // Play-once is OK if still playing.
731  if(current_track) {
732  if(current_track->play_once()) {
733  return;
734  }
735 
736  // If current track no longer on playlist, change it.
737  for(auto m : current_track_list) {
738  if(*current_track == *m) {
739  return;
740  }
741  }
742  }
743 
744  // Victory empties playlist: if next scenario doesn't specify one...
745  if(current_track_list.empty()) {
746  return;
747  }
748 
749  // FIXME: we don't pause ms_before on this first track. Should we?
750  set_previous_track(current_track);
751  current_track = choose_track();
752  play_music();
753 }
754 
756 {
757  // First entry clears playlist, others append to it.
758  bool append = false;
759  for(auto m : current_track_list) {
760  m->write(snapshot, append);
761  append = true;
762  }
763 }
764 
765 void reposition_sound(unsigned id, unsigned int distance)
766 {
767  if(id < track_pool.size()) {
768  if(distance == DISTANCE_SILENT) {
769  MIX_StopTrack(track_pool[id], 0);
770  } else {
771  MIX_Point3D pos;
772  pos.x = 0;
773  pos.y = distance;
774  pos.z = 0;
775  MIX_SetTrack3DPosition(track_pool[id], &pos);
776  }
777  }
778 }
779 
780 bool is_sound_playing(int id)
781 {
782  return MIX_TrackPlaying(track_pool[id]);
783 }
784 
785 void stop_sound(unsigned id)
786 {
788 }
789 
790 namespace
791 {
792 MIX_Track* find_free_track(sound_tracks::type group)
793 {
794  const auto search = [](const auto& span) -> MIX_Track* {
795  for(track_source& t : span) {
796  if(!MIX_TrackPlaying(t)) {
797  return t;
798  }
799  }
800 
801  return nullptr;
802  };
803 
804  switch(group) {
805  case sound_tracks::type::music:
806  return search(music_tracks);
807  case sound_tracks::type::sound_bell:
808  return search(bell_tracks);
809  case sound_tracks::type::sound_timer:
810  return search(timer_tracks);
811  case sound_tracks::type::sound_source:
812  return search(sound_source_tracks);
813  case sound_tracks::type::sound_ui:
814  return search(UI_sound_tracks);
815  case sound_tracks::type::sound_fx:
816  return search(sound_fx_tracks);
817  default:
818  return nullptr;
819  }
820 }
821 
822 void play_sound_internal(const std::string& files,
823  sound_tracks::type group,
824  unsigned int repeats = 0,
825  unsigned int distance = 0,
826  unsigned int soundsource_id = UINT_MAX,
827  const std::chrono::milliseconds& loop_ticks = 0ms,
828  const std::chrono::milliseconds& fadein_ticks = 0ms)
829 {
830  if(files.empty() || !mix_ok) {
831  return;
832  }
833 
834  if(group == sound_tracks::type::sound_source) {
835  if(soundsource_id != UINT_MAX) {
836  std::scoped_lock lock(soundsource_map_mutex);
837  if(soundsource_map.count(soundsource_id) > 0) {
838  return;
839  }
840  } else {
841  return;
842  }
843  }
844 
845  // find a free track in the desired group
846  MIX_Track* free_track = find_free_track(group);
847  if(!free_track) {
848  LOG_AUDIO << "All tracks dedicated to sound group(" << sound_tracks::get_string(group) << ") are busy, skipping.";
849  return;
850  }
851 
852  std::string file = pick_one(files);
853  const auto filename = filesystem::get_binary_file_location("sounds", file);
854  if(!filename) {
855  ERR_AUDIO << "Could not locate sound file '" << file << "'.";
856  return;
857  }
858  const auto localized = filesystem::get_localized_path(filename.value_or(""));
859  std::string real_path = localized.value_or(filename.value());
860 
861  MIX_Point3D pos;
862  pos.x = 0;
863  pos.y = distance;
864  pos.z = 0;
865  MIX_SetTrack3DPosition(free_track, &pos);
866 
867  std::shared_ptr<MIX_Audio> sound;
868  if(sound_cache.count(real_path) != 0) {
869  sound = sound_cache[real_path];
870  DBG_AUDIO << "cache hit for " << real_path;
871  } else {
872  sound.reset(MIX_LoadAudio(mixer, real_path.c_str(), false), &MIX_DestroyAudio);
873  DBG_AUDIO << "cache miss for " << real_path;
874  }
875 
876  MIX_SetTrackAudio(free_track, sound.get());
877 
878  sdl3_properties props;
879 
880  bool res;
881  if(loop_ticks > 0ms) {
882  if(fadein_ticks > 0ms) {
883  SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
884  SDL_SetNumberProperty(props, MIX_PROP_PLAY_MAX_MILLISECONDS_NUMBER, loop_ticks.count());
885  } else {
886  SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
887  }
888  } else {
889  if(fadein_ticks > 0ms) {
890  SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
891  } else {
892  SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, repeats);
893  }
894  }
895 
896  res = MIX_PlayTrack(free_track, props);
897 
898  if(!res) {
899  ERR_AUDIO << "error playing sound effect " << real_path << " : " << SDL_GetError();
900  // still keep it in the sound cache, in case we want to try again later
901  return;
902  } else if(group == sound_tracks::type::sound_source) {
903  // first->first since emplace returns an iterator to a pair (what we actually want) and a boolean
904  // const_cast since the callback signature only accepts void*, not const void*
905  std::scoped_lock lock(soundsource_map_mutex);
906  unsigned int* key = const_cast<unsigned int*>(&(soundsource_map.emplace(soundsource_id, free_track).first->first));
907  DBG_AUDIO << "adding callback for soundsource id " << *key;
908  MIX_SetTrackStoppedCallback(free_track, [](void* userdata, MIX_Track*){
909  std::scoped_lock lock(soundsource_map_mutex);
910  DBG_AUDIO << "in callback to erase soundsource mapping for id " << *static_cast<unsigned int*>(userdata);
911  soundsource_map.erase(*static_cast<unsigned int*>(userdata));
912  }, key);
913  }
914 
915  if(res && sound_cache.count(real_path) == 0) {
916  sound_cache.emplace(real_path, sound);
917  sound_cache_insertion_order.emplace_back(real_path);
918 
919  if(sound_cache.size() > sound_cache_limit) {
920  std::string to_erase = sound_cache_insertion_order[0];
921  DBG_AUDIO << "Uncaching sound file " << to_erase;
922  sound_cache_insertion_order.erase(sound_cache_insertion_order.begin());
923  sound_cache.erase(to_erase);
924  }
925  }
926 }
927 
928 } // namespace
929 
930 void play_sound(const std::string& files, sound_tracks::type group, unsigned int repeats)
931 {
932  if(prefs::get().sound()) {
933  sound::play_sound_internal(files, group, repeats);
934  }
935 }
936 
937 void play_sound_positioned(const std::string& files, int repeats, unsigned int distance, unsigned int id)
938 {
939  if(prefs::get().sound()) {
940  sound::play_sound_internal(files, sound_tracks::type::sound_source, repeats, distance, id);
941  }
942 }
943 
944 // Play bell with separate volume setting
945 void play_bell(const std::string& files)
946 {
947  if(prefs::get().turn_bell()) {
948  sound::play_sound_internal(files, sound_tracks::type::sound_bell);
949  }
950 }
951 
952 // Play timer with separate volume setting
953 void play_timer(const std::string& files, const std::chrono::milliseconds& loop_ticks, const std::chrono::milliseconds& fadein_ticks)
954 {
955  if(prefs::get().sound()) {
956  sound::play_sound_internal(files, sound_tracks::type::sound_timer, 0, DISTANCE_NONE, UINT_MAX, loop_ticks, fadein_ticks);
957  }
958 }
959 
960 // Play UI sounds on separate volume than soundfx
961 void play_UI_sound(const std::string& files)
962 {
963  if(prefs::get().ui_sound_on()) {
964  sound::play_sound_internal(files, sound_tracks::type::sound_ui);
965  }
966 }
967 
969 {
970  if(mix_ok) {
971  return MIX_SetTrackGain(sound::music_tracks[0], -1);
972  }
973 
974  return 0;
975 }
976 
977 void set_music_volume(int vol)
978 {
979  if(mix_ok && vol >= 0) {
980  if(vol > 1.0f) {
981  vol = 1.0f;
982  }
983 
984  MIX_SetTrackGain(sound::music_tracks[0], vol);
985  }
986 }
987 
989 {
990  if(mix_ok) {
991  // Since set_sound_volume sets all main tracks to the same, just return the volume of any main track
992  // FIXME: this is wrong for a function returning int
993  return MIX_GetTrackGain(sound::sound_source_tracks[0]);
994  }
995  return 0;
996 }
997 
998 void set_sound_volume(int vol)
999 {
1000  if(mix_ok && vol >= 0) {
1001  if(vol > 1.0f) {
1002  vol = 1.0f;
1003  }
1004 
1005  // Bell, timer and UI have separate tracks which we can't set up from this
1006  for(track_source& t : sound_source_tracks) {
1007  MIX_SetTrackGain(t, vol);
1008  }
1009 
1010  for(track_source& t : sound_fx_tracks) {
1011  MIX_SetTrackGain(t, vol);
1012  }
1013  }
1014 }
1015 
1016 /*
1017  * For the purpose of volume setting, we treat turn timer the same as bell
1018  */
1019 void set_bell_volume(int vol)
1020 {
1021  if(mix_ok && vol >= 0) {
1022  if(vol > 1.0f) {
1023  vol = 1.0f;
1024  }
1025 
1026  MIX_SetTrackGain(sound::bell_tracks[0], vol);
1027  MIX_SetTrackGain(sound::timer_tracks[0], vol);
1028  }
1029 }
1030 
1031 void set_UI_volume(int vol)
1032 {
1033  if(mix_ok && vol >= 0) {
1034  if(vol > 1.0f) {
1035  vol = 1.0f;
1036  }
1037 
1038  for(track_source& t : UI_sound_tracks) {
1039  MIX_SetTrackGain(t, vol);
1040  }
1041  }
1042 }
1043 
1045 {
1046  music_cache.clear();
1047  music_cache_insertion_order.clear();
1048  sound_cache.clear();
1049  sound_cache_insertion_order.clear();
1050 }
1051 
1052 } // end namespace sound
double t
Definition: astarsearch.cpp:63
constexpr std::enable_if< C==dynamic_extent, span< T, detail::span_sub< E, O >::value > >::type subspan() const
Definition: span.hpp:328
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
virtual void join_global()
Definition: events.cpp:371
bool turn_bell()
static prefs & get()
bool sound()
unsigned int sample_rate()
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:711
Internal representation of music tracks.
static std::shared_ptr< music_track > create(const config &cfg)
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1031
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
Standard logging facilities (interface).
Handling of system events.
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
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:755
void empty_playlist()
Definition: sound.cpp:518
void reposition_sound(unsigned id, unsigned int distance)
Definition: sound.cpp:765
int get_music_volume()
Definition: sound.cpp:968
void set_bell_volume(int vol)
Definition: sound.cpp:1019
void reset_sound()
Definition: sound.cpp:443
bool init_sound()
Definition: sound.cpp:346
void close_sound()
Definition: sound.cpp:413
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:610
void remove_track(unsigned int i)
Definition: sound.cpp:168
void play_music()
Definition: sound.cpp:523
unsigned int get_num_tracks()
Definition: sound.cpp:143
void stop_music()
Definition: sound.cpp:472
void play_music_once(const std::string &file)
Definition: sound.cpp:507
utils::optional< unsigned int > get_current_track_index()
Definition: sound.cpp:123
void set_track(unsigned int i, const std::shared_ptr< music_track > &to)
Definition: sound.cpp:161
void stop_UI_sound()
Definition: sound.cpp:500
std::vector< std::string > enumerate_drivers()
Definition: sound.cpp:316
void play_sound(const std::string &files, sound_tracks::type group, unsigned int repeats)
Definition: sound.cpp:930
void flush_cache()
Definition: sound.cpp:1044
bool is_sound_playing(int id)
Definition: sound.cpp:780
void stop_bell()
Definition: sound.cpp:492
void play_timer(const std::string &files, const std::chrono::milliseconds &loop_ticks, const std::chrono::milliseconds &fadein_ticks)
Definition: sound.cpp:953
int get_sound_volume()
Definition: sound.cpp:988
void play_sound_positioned(const std::string &files, int repeats, unsigned int distance, unsigned int id)
Definition: sound.cpp:937
void commit_music_changes()
Definition: sound.cpp:726
void play_track(unsigned int i)
Definition: sound.cpp:535
void play_UI_sound(const std::string &files)
Definition: sound.cpp:961
void set_previous_track(std::shared_ptr< music_track > track)
Definition: sound.cpp:138
std::shared_ptr< music_track > get_previous_music_track()
Definition: sound.cpp:134
void set_music_volume(int vol)
Definition: sound.cpp:977
std::shared_ptr< music_track > get_track(unsigned int i)
Definition: sound.cpp:148
std::shared_ptr< music_track > get_current_track()
Definition: sound.cpp:130
void stop_sound()
Definition: sound.cpp:481
std::string current_driver()
Definition: sound.cpp:310
void set_UI_volume(int vol)
Definition: sound.cpp:1031
void set_sound_volume(int vol)
Definition: sound.cpp:998
void play_bell(const std::string &files)
Definition: sound.cpp:945
auto find(Container &container, const Value &value, const Projection &projection={})
Definition: general.hpp:196
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.
std::unique_ptr< MIX_Track, decltype(&MIX_DestroyTrack)> track_
Definition: sound.cpp:60
#define DBG_AUDIO
Definition: sound.cpp:34
static lg::log_domain log_audio("audio")
#define ERR_AUDIO
Definition: sound.cpp:36
#define LOG_AUDIO
Definition: sound.cpp:35
#define DISTANCE_NONE
Definition: sound.hpp:69
#define DISTANCE_SILENT
Definition: sound.hpp:68
std::string filename
Filename.
static driver_status query()
Definition: sound.cpp:329
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
Definition: enum_base.hpp:46
static map_location::direction n