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