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