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