The Battle for Wesnoth  1.19.25+dev
sound.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #include "sound.hpp"
17 #include "filesystem.hpp"
18 #include "log.hpp"
20 #include "random.hpp"
23 #include "sound_music_track.hpp"
24 #include "utils/general.hpp"
25 #include "utils/rate_counter.hpp"
26 
27 #include <SDL3/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  if(!filename) {
845  ERR_AUDIO << "Could not locate sound file '" << file << "'.";
846  return;
847  }
848  const auto localized = filesystem::get_localized_path(filename.value_or(""));
849  std::string real_path = localized.value_or(filename.value());
850 
851  MIX_Point3D pos;
852  pos.x = 0;
853  pos.y = distance;
854  pos.z = 0;
855  MIX_SetTrack3DPosition(tracks[free_track].get(), &pos);
856 
857  std::shared_ptr<MIX_Audio> sound;
858  if(sound_cache.count(real_path) != 0) {
859  sound = sound_cache[real_path];
860  DBG_AUDIO << "cache hit for " << real_path;
861  } else {
862  sound.reset(MIX_LoadAudio(mixer, real_path.c_str(), false), &MIX_DestroyAudio);
863  DBG_AUDIO << "cache miss for " << real_path;
864  }
865 
866  MIX_SetTrackAudio(tracks[free_track].get(), sound.get());
867 
868  sdl3_properties props;
869 
870  bool res;
871  if(loop_ticks > 0ms) {
872  if(fadein_ticks > 0ms) {
873  SDL_SetNumberProperty(props.id(), MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
874  SDL_SetNumberProperty(props.id(), MIX_PROP_PLAY_MAX_MILLISECONDS_NUMBER, loop_ticks.count());
875  } else {
876  SDL_SetNumberProperty(props.id(), MIX_PROP_PLAY_LOOPS_NUMBER, -1);
877  }
878  } else {
879  if(fadein_ticks > 0ms) {
880  SDL_SetNumberProperty(props.id(), MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
881  } else {
882  SDL_SetNumberProperty(props.id(), MIX_PROP_PLAY_LOOPS_NUMBER, repeats);
883  }
884  }
885 
886  res = MIX_PlayTrack(tracks[free_track].get(), props.id());
887 
888  if(!res) {
889  ERR_AUDIO << "error playing sound effect " << real_path << " : " << SDL_GetError();
890  // still keep it in the sound cache, in case we want to try again later
891  return;
892  } else if(group == sound_tracks::type::sound_source) {
893  // first->first since emplace returns an iterator to a pair (what we actually want) and a boolean
894  // const_cast since the callback signature only accepts void*, not const void*
895  std::scoped_lock lock(soundsource_map_mutex);
896  unsigned int* key = const_cast<unsigned int*>(&(soundsource_map.emplace(soundsource_id, free_track).first->first));
897  DBG_AUDIO << "adding callback for soundsource id " << *key;
898  MIX_SetTrackStoppedCallback(tracks[free_track].get(), [](void* userdata, MIX_Track*){
899  std::scoped_lock lock(soundsource_map_mutex);
900  DBG_AUDIO << "in callback to erase soundsource mapping for id " << *static_cast<unsigned int*>(userdata);
901  soundsource_map.erase(*static_cast<unsigned int*>(userdata));
902  }, key);
903  }
904 
905  if(res && sound_cache.count(real_path) == 0) {
906  sound_cache.emplace(real_path, sound);
907  sound_cache_insertion_order.emplace_back(real_path);
908 
909  if(sound_cache.size() > sound_cache_limit) {
910  std::string to_erase = sound_cache_insertion_order[0];
911  DBG_AUDIO << "Uncaching sound file " << to_erase;
913  sound_cache.erase(to_erase);
914  }
915  }
916 }
917 
918 } // end anon namespace
919 
920 namespace sound {
921 void play_sound(const std::string& files, sound_tracks::type group, unsigned int repeats)
922 {
923  if(prefs::get().sound()) {
924  sound::play_sound_internal(files, group, repeats);
925  }
926 }
927 
928 void play_sound_positioned(const std::string& files, int repeats, unsigned int distance, unsigned int id)
929 {
930  if(prefs::get().sound()) {
931  sound::play_sound_internal(files, sound_tracks::type::sound_source, repeats, distance, id);
932  }
933 }
934 
935 // Play bell with separate volume setting
936 void play_bell(const std::string& files)
937 {
938  if(prefs::get().turn_bell()) {
939  sound::play_sound_internal(files, sound_tracks::type::sound_bell);
940  }
941 }
942 
943 // Play timer with separate volume setting
944 void play_timer(const std::string& files, const std::chrono::milliseconds& loop_ticks, const std::chrono::milliseconds& fadein_ticks)
945 {
946  if(prefs::get().sound()) {
947  sound::play_sound_internal(files, sound_tracks::type::sound_timer, 0, DISTANCE_NONE, UINT_MAX, loop_ticks, fadein_ticks);
948  }
949 }
950 
951 // Play UI sounds on separate volume than soundfx
952 void play_UI_sound(const std::string& files)
953 {
954  if(prefs::get().ui_sound_on()) {
955  sound::play_sound_internal(files, sound_tracks::type::sound_ui);
956  }
957 }
958 
960 {
961  if(mix_ok) {
962  return MIX_SetTrackGain(sound::tracks[music_track_id].get(), -1);
963  }
964 
965  return 0;
966 }
967 
968 void set_music_volume(int vol)
969 {
970  if(mix_ok && vol >= 0) {
971  if(vol > 1.0f) {
972  vol = 1.0f;
973  }
974 
975  MIX_SetTrackGain(sound::tracks[music_track_id].get(), vol);
976  }
977 }
978 
980 {
981  if(mix_ok) {
982  // Since set_sound_volume sets all main tracks to the same, just return the volume of any main track
983  return MIX_SetTrackGain(sound::tracks[source_track_id_start].get(), -1);
984  }
985  return 0;
986 }
987 
988 void set_sound_volume(int vol)
989 {
990  if(mix_ok && vol >= 0) {
991  if(vol > 1.0f) {
992  vol = 1.0f;
993  }
994 
995  // Bell, timer and UI have separate tracks which we can't set up from this
996  // Also separating music track from being modified
997  for(unsigned i = 0; i < n_of_tracks; ++i) {
998  if(i != music_track_id && !(i >= UI_sound_track_id_start && i <= UI_sound_track_id_last) && i != bell_track_id && i != timer_track_id) { MIX_SetTrackGain(sound::tracks[i].get(), vol);
999  }
1000  }
1001  }
1002 }
1003 
1004 /*
1005  * For the purpose of volume setting, we treat turn timer the same as bell
1006  */
1007 void set_bell_volume(int vol)
1008 {
1009  if(mix_ok && vol >= 0) {
1010  if(vol > 1.0f) {
1011  vol = 1.0f;
1012  }
1013 
1014  MIX_SetTrackGain(sound::tracks[bell_track_id].get(), vol);
1015  MIX_SetTrackGain(sound::tracks[timer_track_id].get(), vol);
1016  }
1017 }
1018 
1019 void set_UI_volume(int vol)
1020 {
1021  if(mix_ok && vol >= 0) {
1022  if(vol > 1.0f) {
1023  vol = 1.0f;
1024  }
1025 
1026  for(unsigned i = UI_sound_track_id_start; i <= UI_sound_track_id_last; ++i) {
1027  MIX_SetTrackGain(sound::tracks[i].get(), vol);
1028  }
1029  }
1030 }
1031 
1033 {
1034  music_cache.clear();
1036  sound_cache.clear();
1038 }
1039 
1040 } // 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:205
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:959
std::vector< std::shared_ptr< MIX_Track > > tracks
Definition: sound.cpp:44
void set_bell_volume(int vol)
Definition: sound.cpp:1007
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:921
std::size_t mixer_init_counter
Definition: sound.cpp:54
void flush_cache()
Definition: sound.cpp:1032
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:944
int get_sound_volume()
Definition: sound.cpp:979
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:928
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:952
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:968
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:1019
void set_sound_volume(int vol)
Definition: sound.cpp:988
MIX_Mixer * mixer
Definition: sound.cpp:53
void play_bell(const std::string &files)
Definition: sound.cpp:936
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