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