The Battle for Wesnoth  1.19.26+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 channel
44 {
45 public:
46  /** Creates a new type-tagged track on @a mixer. */
47  void allocate_channel(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<channel, 32> channel_pool{};
67 
68 // One of these can play at a time
69 const auto music_channels = utils::span{channel_pool}.subspan<0, 1>();
70 const auto bell_channels = utils::span{channel_pool}.subspan<1, 1>();
71 const auto timer_channels = utils::span{channel_pool}.subspan<2, 1>();
72 
73 // Several of these can play at a time
74 const auto positional_channels = utils::span{channel_pool}.subspan<3, 8>();
75 const auto UI_channels = utils::span{channel_pool}.subspan<11, 2>();
76 const auto SFX_channels = utils::span{channel_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(channel& c : music_channels) {
378  c.allocate_channel(mixer, sound_tracks::music);
379  }
380 
381  for(channel& c : bell_channels) {
382  c.allocate_channel(mixer, sound_tracks::sound_bell);
383  }
384 
385  for(channel& c : timer_channels) {
386  c.allocate_channel(mixer, sound_tracks::sound_timer);
387  }
388 
389  for(channel& c : positional_channels) {
390  c.allocate_channel(mixer, sound_tracks::sound_source);
391  }
392 
393  for(channel& c : UI_channels) {
394  c.allocate_channel(mixer, sound_tracks::sound_ui);
395  }
396 
397  for(channel& c : SFX_channels) {
398  c.allocate_channel(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  channel_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(channel& c : music_channels) {
476  MIX_StopTrack(c, MIX_TrackMSToFrames(c, 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_channels[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_channels[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_channels[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 MIX_Track* get_positional_channel(unsigned soundsource_id)
609 {
610  std::scoped_lock lock{soundsource_map_mutex};
611  const auto it = soundsource_map.find(soundsource_id);
612  return it == soundsource_map.end() ? nullptr : it->second;
613 }
614 
615 } // namespace
616 
617 void play_music_config(const config& music_node, bool allow_interrupt_current_track, int i)
618 {
619  //
620  // FIXME: there is a memory leak somewhere in this function, seemingly related to the shared_ptrs
621  // stored in current_track_list.
622  //
623  // vultraz 5/8/2017
624  //
625 
626  auto track = sound::music_track::create(music_node);
627  if(!track) {
628  ERR_AUDIO << "cannot open track; disabled in this playlist.";
629  return;
630  }
631 
632  // If they say play once, we don't alter playlist.
633  if(track->play_once()) {
634  set_previous_track(current_track);
635  current_track = std::move(track);
636  current_track_index = current_track_list.size();
637  play_music();
638  return;
639  }
640 
641  // Clear play list unless they specify append.
642  if(!track->append()) {
643  current_track_list.clear();
644  }
645 
646  auto iter = find_track(*track);
647  // Avoid 2 tracks with the same name, since that can cause an infinite loop
648  // in choose_track(), 2 tracks with the same name will always return the
649  // current track and track_ok() doesn't allow that.
650  if(iter == current_track_list.end()) {
651  auto insert_at = (i >= 0 && static_cast<std::size_t>(i) < current_track_list.size())
652  ? current_track_list.begin() + i
653  : current_track_list.end();
654 
655  // Copy the track pointer so our local variable remains non-null.
656  iter = current_track_list.insert(insert_at, track);
657  auto new_track_index = std::distance(current_track_list.cbegin(), iter);
658 
659  // If we inserted the new track *before* the current track, adjust
660  // cached index so it still points to the same element.
661  if(new_track_index <= current_track_index) {
662  ++current_track_index;
663  }
664  } else {
665  ERR_AUDIO << "tried to add duplicate track '" << track->file_path() << "'";
666  }
667 
668  // They can tell us to start playing this list immediately.
669  if(track->immediate()) {
670  set_previous_track(current_track);
671  current_track = *iter;
672  current_track_index = std::distance(current_track_list.cbegin(), iter);
673  play_music();
674  } else if(!track->append() && !allow_interrupt_current_track && current_track) {
675  // Make sure the current track will finish first
676  current_track->set_play_once(true);
677  }
678 }
679 
681 {
682  if(MIX_GetTrackFadeFrames(music_channels[0]) != 0) {
683  // Do not block everything while fading.
684  return;
685  }
686 
687  if(prefs::get().music_on()) {
688  // TODO: rethink the music_thinker design, especially the use of fade_out_time
689  auto now = std::chrono::steady_clock::now();
690 
691  bool is_playing = MIX_TrackPlaying(music_channels[0]);
692  bool is_paused = MIX_TrackPaused(music_channels[0]);
693  if(!music_start_time && !current_track_list.empty() && !is_playing && !is_paused) {
694  // Pick next track, add ending time to its start time.
695  set_previous_track(current_track);
696  current_track = choose_track();
697  music_start_time = now;
698  no_fading = true;
699  fade_out_time = 0ms;
700  }
701 
702  if(music_start_time && music_refresh_rate.poll()) {
703  want_new_music = now >= *music_start_time - fade_out_time;
704  }
705 
706  if(want_new_music) {
707  if(MIX_TrackPlaying(music_channels[0])) {
708  MIX_StopTrack(music_channels[0], MIX_TrackMSToFrames(music_channels[0], fade_out_time.count()));
709  return;
710  }
711 
712  play_new_music();
713  }
714  }
715 }
716 
718  : events::sdl_handler(false)
719 {
720  join_global();
721 }
722 
723 void music_muter::handle_window_event(const SDL_Event& event)
724 {
725  if(prefs::get().stop_music_in_background() && prefs::get().music_on()) {
726  if(event.type == SDL_EVENT_WINDOW_FOCUS_GAINED) {
727  MIX_ResumeTrack(music_channels[0]);
728  DBG_AUDIO << "resuming music";
729  } else if(event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
730  if(MIX_TrackPlaying(music_channels[0])) {
731  MIX_PauseTrack(music_channels[0]);
732  DBG_AUDIO << "pausing music";
733  }
734  }
735  }
736 }
737 
739 {
740  played_before.clear();
741 
742  // Play-once is OK if still playing.
743  if(current_track) {
744  if(current_track->play_once()) {
745  return;
746  }
747 
748  // If current track no longer on playlist, change it.
749  for(auto m : current_track_list) {
750  if(*current_track == *m) {
751  return;
752  }
753  }
754  }
755 
756  // Victory empties playlist: if next scenario doesn't specify one...
757  if(current_track_list.empty()) {
758  return;
759  }
760 
761  // FIXME: we don't pause ms_before on this first track. Should we?
762  set_previous_track(current_track);
763  current_track = choose_track();
764  play_music();
765 }
766 
768 {
769  // First entry clears playlist, others append to it.
770  bool append = false;
771  for(auto m : current_track_list) {
772  m->write(snapshot, append);
773  append = true;
774  }
775 }
776 
777 void reposition_sound(unsigned id, unsigned int distance)
778 {
779  if(MIX_Track* track = get_positional_channel(id)) {
780  if(distance == distance_silent) {
781  MIX_StopTrack(track, 0);
782  } else {
783  MIX_Point3D pos;
784  pos.x = 0;
785  pos.y = distance;
786  pos.z = 0;
787  MIX_SetTrack3DPosition(track, &pos);
788  }
789  }
790 }
791 
792 bool is_sound_playing(int id)
793 {
794  MIX_Track* track = get_positional_channel(id);
795  return track && MIX_TrackPlaying(track);
796 }
797 
798 void stop_sound(unsigned id)
799 {
801 }
802 
803 namespace
804 {
805 MIX_Track* find_free_channel(sound_tracks::type group)
806 {
807  const auto search = [](const auto& span) -> MIX_Track* {
808  for(channel& c : span) {
809  if(!MIX_TrackPlaying(c)) {
810  return c;
811  }
812  }
813 
814  return nullptr;
815  };
816 
817  switch(group) {
818  case sound_tracks::type::music:
819  return search(music_channels);
820  case sound_tracks::type::sound_bell:
821  return search(bell_channels);
822  case sound_tracks::type::sound_timer:
823  return search(timer_channels);
824  case sound_tracks::type::sound_source:
825  return search(positional_channels);
826  case sound_tracks::type::sound_ui:
827  return search(UI_channels);
828  case sound_tracks::type::sound_fx:
829  return search(SFX_channels);
830  default:
831  return nullptr;
832  }
833 }
834 
835 void play_sound_internal(const std::string& files,
836  sound_tracks::type group,
837  unsigned int repeats = 0,
838  unsigned int distance = 0,
839  unsigned int soundsource_id = UINT_MAX,
840  const std::chrono::milliseconds& loop_ticks = 0ms,
841  const std::chrono::milliseconds& fadein_ticks = 0ms)
842 {
843  if(files.empty() || !mix_ok) {
844  return;
845  }
846 
847  if(group == sound_tracks::type::sound_source) {
848  if(soundsource_id != UINT_MAX) {
849  if(is_sound_playing(soundsource_id)) {
850  return;
851  }
852  } else {
853  return;
854  }
855  }
856 
857  // find a free track in the desired group
858  MIX_Track* free_channel = find_free_channel(group);
859  if(!free_channel) {
860  LOG_AUDIO << "All tracks dedicated to sound group(" << sound_tracks::get_string(group) << ") are busy, skipping.";
861  return;
862  }
863 
864  std::string file = pick_one(files);
865  const auto filename = filesystem::get_binary_file_location("sounds", file);
866  if(!filename) {
867  ERR_AUDIO << "Could not locate sound file '" << file << "'.";
868  return;
869  }
870  const auto localized = filesystem::get_localized_path(filename.value_or(""));
871  std::string real_path = localized.value_or(filename.value());
872 
873  MIX_Point3D pos;
874  pos.x = 0;
875  pos.y = distance;
876  pos.z = 0;
877  MIX_SetTrack3DPosition(free_channel, &pos);
878 
879  std::shared_ptr<MIX_Audio> sound;
880  if(sound_cache.count(real_path) != 0) {
881  sound = sound_cache[real_path];
882  DBG_AUDIO << "cache hit for " << real_path;
883  } else {
884  sound.reset(MIX_LoadAudio(mixer, real_path.c_str(), false), &MIX_DestroyAudio);
885  DBG_AUDIO << "cache miss for " << real_path;
886  }
887 
888  MIX_SetTrackAudio(free_channel, sound.get());
889 
890  sdl3_properties props;
891 
892  bool res;
893  if(loop_ticks > 0ms) {
894  if(fadein_ticks > 0ms) {
895  SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
896  SDL_SetNumberProperty(props, MIX_PROP_PLAY_MAX_MILLISECONDS_NUMBER, loop_ticks.count());
897  } else {
898  SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
899  }
900  } else {
901  if(fadein_ticks > 0ms) {
902  SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
903  } else {
904  SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, repeats);
905  }
906  }
907 
908  res = MIX_PlayTrack(free_channel, props);
909 
910  if(!res) {
911  ERR_AUDIO << "error playing sound effect " << real_path << " : " << SDL_GetError();
912  // still keep it in the sound cache, in case we want to try again later
913  return;
914  } else if(group == sound_tracks::type::sound_source) {
915  // first->first since emplace returns an iterator to a pair (what we actually want) and a boolean
916  // const_cast since the callback signature only accepts void*, not const void*
917  std::scoped_lock lock(soundsource_map_mutex);
918  unsigned int* key = const_cast<unsigned int*>(&(soundsource_map.emplace(soundsource_id, free_channel).first->first));
919  DBG_AUDIO << "adding callback for soundsource id " << *key;
920  MIX_SetTrackStoppedCallback(free_channel, [](void* userdata, MIX_Track*){
921  std::scoped_lock lock(soundsource_map_mutex);
922  DBG_AUDIO << "in callback to erase soundsource mapping for id " << *static_cast<unsigned int*>(userdata);
923  soundsource_map.erase(*static_cast<unsigned int*>(userdata));
924  }, key);
925  }
926 
927  if(res && sound_cache.count(real_path) == 0) {
928  sound_cache.emplace(real_path, sound);
929  sound_cache_insertion_order.emplace_back(real_path);
930 
931  if(sound_cache.size() > sound_cache_limit) {
932  std::string to_erase = sound_cache_insertion_order[0];
933  DBG_AUDIO << "Uncaching sound file " << to_erase;
934  sound_cache_insertion_order.erase(sound_cache_insertion_order.begin());
935  sound_cache.erase(to_erase);
936  }
937  }
938 }
939 
940 /** Clamp gain value to a sensible (albeit arbitrary) range. */
941 volume clamp_gain(volume value)
942 {
943  return std::clamp(value, sound::silence, sound::max_volume);
944 }
945 
946 } // namespace
947 
948 void play_sound(const std::string& files, sound_tracks::type group, unsigned int repeats)
949 {
950  if(prefs::get().sound()) {
951  sound::play_sound_internal(files, group, repeats);
952  }
953 }
954 
955 void play_sound_positioned(const std::string& files, int repeats, unsigned int distance, unsigned int id)
956 {
957  if(prefs::get().sound()) {
958  sound::play_sound_internal(files, sound_tracks::type::sound_source, repeats, distance, id);
959  }
960 }
961 
962 // Play bell with separate volume setting
963 void play_bell(const std::string& files)
964 {
965  if(prefs::get().turn_bell()) {
966  sound::play_sound_internal(files, sound_tracks::type::sound_bell);
967  }
968 }
969 
970 // Play timer with separate volume setting
971 void play_timer(const std::string& files, const std::chrono::milliseconds& loop_ticks, const std::chrono::milliseconds& fadein_ticks)
972 {
973  if(prefs::get().sound()) {
974  sound::play_sound_internal(files, sound_tracks::type::sound_timer, 0, distance_none, UINT_MAX, loop_ticks, fadein_ticks);
975  }
976 }
977 
978 // Play UI sounds on separate volume than soundfx
979 void play_UI_sound(const std::string& files)
980 {
981  if(prefs::get().ui_sound_on()) {
982  sound::play_sound_internal(files, sound_tracks::type::sound_ui);
983  }
984 }
985 
987 {
988  if(mix_ok) {
989  return volume{MIX_GetTrackGain(sound::music_channels[0])};
990  }
991 
992  return sound::silence;
993 }
994 
996 {
997  if(mix_ok) {
998  MIX_SetTrackGain(sound::music_channels[0], clamp_gain(vol));
999  }
1000 }
1001 
1003 {
1004  if(mix_ok) {
1005  // Since set_sound_volume sets all main tracks to the same, just return the volume of any main track
1006  return volume{MIX_GetTrackGain(sound::positional_channels[0])};
1007  }
1008 
1009  return sound::silence;
1010 }
1011 
1013 {
1014  if(mix_ok) {
1015  vol = clamp_gain(vol);
1016 
1017  // Bell, timer and UI have separate tracks which we can't set up from this
1018  for(channel& c : positional_channels) {
1019  MIX_SetTrackGain(c, vol);
1020  }
1021 
1022  for(channel& c : SFX_channels) {
1023  MIX_SetTrackGain(c, vol);
1024  }
1025  }
1026 }
1027 
1028 /*
1029  * For the purpose of volume setting, we treat turn timer the same as bell
1030  */
1032 {
1033  if(mix_ok) {
1034  vol = clamp_gain(vol);
1035 
1036  MIX_SetTrackGain(sound::bell_channels[0], vol);
1037  MIX_SetTrackGain(sound::timer_channels[0], vol);
1038  }
1039 }
1040 
1042 {
1043  if(mix_ok) {
1044  vol = clamp_gain(vol);
1045 
1046  for(channel& c : UI_channels) {
1047  MIX_SetTrackGain(c, vol);
1048  }
1049  }
1050 }
1051 
1053 {
1054  music_cache.clear();
1055  music_cache_insertion_order.clear();
1056  sound_cache.clear();
1057  sound_cache_insertion_order.clear();
1058 }
1059 
1060 } // end namespace sound
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:723
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: preferences.hpp:69
void write_music_play_list(config &snapshot)
Definition: sound.cpp:767
void empty_playlist()
Definition: sound.cpp:518
void reposition_sound(unsigned id, unsigned int distance)
Definition: sound.cpp:777
void set_UI_volume(volume vol)
Definition: sound.cpp:1041
void reset_sound()
Definition: sound.cpp:443
void set_music_volume(volume vol)
Definition: sound.cpp:995
bool init_sound()
Definition: sound.cpp:346
void close_sound()
Definition: sound.cpp:413
constexpr volume silence
Definition: sound.hpp:144
volume get_music_volume()
Definition: sound.cpp:986
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:617
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
constexpr int distance_none
Definition: sound.hpp:70
void stop_music()
Definition: sound.cpp:472
void set_sound_volume(volume vol)
Definition: sound.cpp:1012
void play_music_once(const std::string &file)
Definition: sound.cpp:507
constexpr int distance_silent
Definition: sound.hpp:69
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:948
void flush_cache()
Definition: sound.cpp:1052
void set_bell_volume(volume vol)
Definition: sound.cpp:1031
bool is_sound_playing(int id)
Definition: sound.cpp:792
void stop_bell()
Definition: sound.cpp:492
volume get_sound_volume()
Definition: sound.cpp:1002
void play_timer(const std::string &files, const std::chrono::milliseconds &loop_ticks, const std::chrono::milliseconds &fadein_ticks)
Definition: sound.cpp:971
void play_sound_positioned(const std::string &files, int repeats, unsigned int distance, unsigned int id)
Definition: sound.cpp:955
void commit_music_changes()
Definition: sound.cpp:738
void play_track(unsigned int i)
Definition: sound.cpp:535
constexpr volume max_volume
Definition: sound.hpp:146
void play_UI_sound(const std::string &files)
Definition: sound.cpp:979
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
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 play_bell(const std::string &files)
Definition: sound.cpp:963
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
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
mock_char c
static map_location::direction n
channel
Definition: utils.hpp:103