27 #include <SDL3_mixer/SDL_mixer.h>
34 #define DBG_AUDIO LOG_STREAM(debug, log_audio)
35 #define LOG_AUDIO LOG_STREAM(info, log_audio)
36 #define ERR_AUDIO LOG_STREAM(err, log_audio)
47 void allocate_track(MIX_Mixer* mixer,
const char* type_tag)
49 track_.reset(MIX_CreateTrack(mixer));
50 MIX_TagTrack(*
this, type_tag);
54 operator MIX_Track*()
const
60 std::unique_ptr<MIX_Track, decltype(&MIX_DestroyTrack)>
track_{
nullptr, &MIX_DestroyTrack};
63 std::mutex soundsource_map_mutex;
64 std::map<unsigned int, MIX_Track*> soundsource_map;
66 std::array<track_source, 32> track_pool{};
79 std::map<std::string, std::shared_ptr<MIX_Audio>> music_cache;
80 std::vector<std::string> music_cache_insertion_order;
82 std::map<std::string, std::shared_ptr<MIX_Audio>> sound_cache;
83 std::vector<std::string> sound_cache_insertion_order;
86 std::size_t mixer_init_counter = 0;
88 using namespace std::chrono_literals;
91 utils::optional<std::chrono::steady_clock::time_point> music_start_time;
93 bool want_new_music =
false;
94 auto fade_out_time = 5000ms;
95 bool no_fading =
false;
97 const std::size_t music_cache_limit = 30;
98 const std::size_t sound_cache_limit = 500;
100 std::vector<std::string> played_before;
110 std::vector<std::shared_ptr<sound::music_track>> current_track_list;
111 std::shared_ptr<sound::music_track> current_track;
112 unsigned int current_track_index = 0;
113 std::shared_ptr<sound::music_track> previous_track;
115 std::vector<std::shared_ptr<sound::music_track>>::const_iterator find_track(
const sound::music_track& track)
118 [](
const std::shared_ptr<const sound::music_track>& ptr) {
return *ptr; });
125 if(current_track_index >= current_track_list.size()){
128 return current_track_index;
132 return current_track;
136 return previous_track;
140 previous_track = std::move(track);
145 return current_track_list.size();
150 if(
i < current_track_list.size()) {
151 return current_track_list[
i];
154 if(
i == current_track_list.size()) {
155 return current_track;
161 void set_track(
unsigned int i,
const std::shared_ptr<music_track>& to)
163 if(
i < current_track_list.size() && find_track(*to) != current_track_list.end()) {
164 current_track_list[
i] = std::make_shared<music_track>(*to);
170 if(
i >= current_track_list.size()) {
174 if(
i == current_track_index) {
177 current_track->set_play_once(
true);
180 current_track_index = current_track_list.size() - 1;
181 }
else if(
i < current_track_index) {
182 current_track_index--;
185 current_track_list.erase(current_track_list.begin() +
i);
190 bool track_ok(
const std::string&
id)
200 if(
id == current_track->file_path()) {
204 if(current_track_list.size() <= 3) {
216 unsigned int num_played = 0;
217 std::set<std::string> played;
218 std::vector<std::string>::reverse_iterator
i;
220 for(
i = played_before.rbegin();
i != played_before.rend(); ++
i) {
223 if(num_played == 2) {
232 if(num_played == 2 && played.size() != current_track_list.size() - 1) {
233 LOG_AUDIO <<
"Played twice with only " << played.size() <<
" tracks between";
238 i = played_before.rbegin();
239 if(
i != played_before.rend()) {
241 if(
i != played_before.rend()) {
243 LOG_AUDIO <<
"Played just before previous";
252 std::shared_ptr<sound::music_track> choose_track()
254 assert(!current_track_list.empty());
256 if(current_track_index >= current_track_list.size()) {
257 current_track_index = 0;
260 if(current_track_list[current_track_index]->
shuffle()) {
261 unsigned int track = 0;
263 if(current_track_list.size() > 1) {
266 }
while(!track_ok(current_track_list[track]->file_path()));
269 current_track_index = track;
272 DBG_AUDIO <<
"Next track will be " << current_track_list[current_track_index]->file_path();
273 played_before.push_back(current_track_list[current_track_index]->file_path());
274 return current_track_list[current_track_index];
277 std::string pick_one(
const std::string& files)
285 if(ids.size() == 1) {
290 static std::map<std::string, unsigned int> prev_choices;
293 if(prev_choices.find(files) != prev_choices.end()) {
295 if(choice >= prev_choices[files]) {
299 prev_choices[files] = choice;
302 prev_choices.emplace(files, choice);
312 const char*
const drvname = SDL_GetCurrentAudioDriver();
313 return drvname ? drvname :
"<not initialized>";
318 std::vector<std::string> res;
319 int num_drivers = SDL_GetNumVideoDrivers();
321 for(
int n = 0;
n < num_drivers; ++
n) {
322 const char* drvname = SDL_GetAudioDriver(
n);
323 res.emplace_back(drvname ? drvname :
"<invalid driver>");
335 if(MIX_GetMixerFormat(mixer, &spec)) {
337 res.frequency = spec.freq;
338 res.format = spec.format;
339 res.channels = spec.channels;
349 if(SDL_WasInit(SDL_INIT_AUDIO) == 0) {
350 if(!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
351 ERR_AUDIO <<
"Could not initialize audio: " << SDL_GetError();
357 mixer_init_counter++;
359 ERR_AUDIO <<
"Could not initialize mixer: " << SDL_GetError();
366 spec.format = SDL_AUDIO_S16;
368 mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec);
371 ERR_AUDIO <<
"Could not initialize audio: " << SDL_GetError();
377 for(track_source&
t : music_tracks) {
378 t.allocate_track(mixer, sound_tracks::music);
381 for(track_source&
t : bell_tracks) {
382 t.allocate_track(mixer, sound_tracks::sound_bell);
385 for(track_source&
t : timer_tracks) {
386 t.allocate_track(mixer, sound_tracks::sound_timer);
389 for(track_source&
t : sound_source_tracks) {
390 t.allocate_track(mixer, sound_tracks::sound_source);
393 for(track_source&
t : UI_sound_tracks) {
394 t.allocate_track(mixer, sound_tracks::sound_ui);
397 for(track_source&
t : sound_fx_tracks) {
398 t.allocate_track(mixer, sound_tracks::sound_fx);
425 MIX_DestroyMixer(mixer);
431 for(std::size_t
i = 0;
i < mixer_init_counter;
i++) {
434 mixer_init_counter = 0;
436 if(SDL_WasInit(SDL_INIT_AUDIO) != 0) {
437 SDL_QuitSubSystem(SDL_INIT_AUDIO);
450 if(music ||
sound || bell || UI_sound) {
475 for(track_source&
t : music_tracks) {
476 MIX_StopTrack(
t, MIX_TrackMSToFrames(
t, 500));
484 MIX_StopTag(mixer, sound_tracks::sound_source, 0);
485 MIX_StopTag(mixer, sound_tracks::sound_fx, 0);
495 MIX_StopTag(mixer, sound_tracks::sound_bell, 0);
496 MIX_StopTag(mixer, sound_tracks::sound_timer, 0);
503 MIX_StopTag(mixer, sound_tracks::sound_ui, 0);
511 current_track = std::move(track);
512 current_track->set_play_once(
true);
513 current_track_index = current_track_list.size();
520 current_track_list.clear();
529 music_start_time = std::chrono::steady_clock::now();
530 want_new_music =
true;
532 fade_out_time = previous_track !=
nullptr ? previous_track->ms_after() : 0ms;
538 if(
i >= current_track_list.size()) {
539 current_track = choose_track();
541 current_track_index =
i;
542 current_track = current_track_list[
i];
549 void play_new_music()
551 music_start_time.reset();
552 want_new_music =
true;
554 if(!
prefs::get().music_on() || !mix_ok || !current_track) {
558 std::string
filename = current_track->file_path();
564 auto fading_time = current_track->ms_before();
574 MIX_StopTrack(music_tracks[0], 0);
576 std::shared_ptr<MIX_Audio> music;
577 if(music_cache.count(
filename) != 0) {
581 music.reset(MIX_LoadAudio(mixer,
filename.c_str(),
false), &MIX_DestroyAudio);
586 MIX_SetTrackAudio(music_tracks[0], music.get());
589 SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fading_time.count());
591 if(!MIX_PlayTrack(music_tracks[0], props)) {
592 ERR_AUDIO <<
"Could not play music: " << SDL_GetError() <<
" " <<
filename <<
" ";
593 }
else if(music_cache.count(
filename) == 0) {
594 music_cache.emplace(
filename, music);
595 music_cache_insertion_order.emplace_back(
filename);
597 if(music_cache.size() > music_cache_limit) {
598 std::string to_erase = music_cache_insertion_order[0];
599 DBG_AUDIO <<
"Uncaching music file " << to_erase;
600 music_cache_insertion_order.erase(music_cache_insertion_order.begin());
601 music_cache.erase(to_erase);
605 want_new_music =
false;
621 ERR_AUDIO <<
"cannot open track; disabled in this playlist.";
626 if(track->play_once()) {
628 current_track = std::move(track);
629 current_track_index = current_track_list.size();
635 if(!track->append()) {
636 current_track_list.clear();
639 auto iter = find_track(*track);
643 if(iter == current_track_list.end()) {
644 auto insert_at = (
i >= 0 &&
static_cast<std::size_t
>(
i) < current_track_list.size())
645 ? current_track_list.begin() +
i
646 : current_track_list.end();
649 iter = current_track_list.insert(insert_at, track);
650 auto new_track_index = std::distance(current_track_list.cbegin(), iter);
654 if(new_track_index <= current_track_index) {
655 ++current_track_index;
658 ERR_AUDIO <<
"tried to add duplicate track '" << track->file_path() <<
"'";
662 if(track->immediate()) {
664 current_track = *iter;
665 current_track_index = std::distance(current_track_list.cbegin(), iter);
667 }
else if(!track->append() && !allow_interrupt_current_track && current_track) {
669 current_track->set_play_once(
true);
677 auto now = std::chrono::steady_clock::now();
679 bool is_playing = MIX_TrackPlaying(music_tracks[0]);
680 bool is_paused = MIX_TrackPaused(music_tracks[0]);
681 if(!music_start_time && !current_track_list.empty() && !is_playing && !is_paused) {
684 current_track = choose_track();
685 music_start_time = now;
690 if(music_start_time && music_refresh_rate.poll()) {
691 want_new_music = now >= *music_start_time - fade_out_time;
695 if(MIX_TrackPlaying(music_tracks[0])) {
696 MIX_StopTrack(music_tracks[0], MIX_TrackMSToFrames(music_tracks[0], fade_out_time.count()));
706 :
events::sdl_handler(false)
714 if(event.type == SDL_EVENT_WINDOW_FOCUS_GAINED) {
715 MIX_ResumeTrack(music_tracks[0]);
717 }
else if(event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
718 if(MIX_TrackPlaying(music_tracks[0])) {
719 MIX_PauseTrack(music_tracks[0]);
728 played_before.clear();
732 if(current_track->play_once()) {
737 for(
auto m : current_track_list) {
738 if(*current_track == *m) {
745 if(current_track_list.empty()) {
751 current_track = choose_track();
759 for(
auto m : current_track_list) {
760 m->write(snapshot, append);
767 if(
id < track_pool.size()) {
769 MIX_StopTrack(track_pool[
id], 0);
775 MIX_SetTrack3DPosition(track_pool[
id], &pos);
782 return MIX_TrackPlaying(track_pool[
id]);
794 const auto search = [](
const auto& span) -> MIX_Track* {
795 for(track_source&
t : span) {
796 if(!MIX_TrackPlaying(
t)) {
805 case sound_tracks::type::music:
806 return search(music_tracks);
807 case sound_tracks::type::sound_bell:
808 return search(bell_tracks);
809 case sound_tracks::type::sound_timer:
810 return search(timer_tracks);
811 case sound_tracks::type::sound_source:
812 return search(sound_source_tracks);
813 case sound_tracks::type::sound_ui:
814 return search(UI_sound_tracks);
815 case sound_tracks::type::sound_fx:
816 return search(sound_fx_tracks);
822 void play_sound_internal(
const std::string& files,
824 unsigned int repeats = 0,
825 unsigned int distance = 0,
826 unsigned int soundsource_id = UINT_MAX,
827 const std::chrono::milliseconds& loop_ticks = 0ms,
828 const std::chrono::milliseconds& fadein_ticks = 0ms)
830 if(files.empty() || !mix_ok) {
834 if(group == sound_tracks::type::sound_source) {
835 if(soundsource_id != UINT_MAX) {
836 std::scoped_lock lock(soundsource_map_mutex);
837 if(soundsource_map.count(soundsource_id) > 0) {
846 MIX_Track* free_track = find_free_track(group);
852 std::string file = pick_one(files);
855 ERR_AUDIO <<
"Could not locate sound file '" << file <<
"'.";
859 std::string real_path = localized.value_or(
filename.value());
865 MIX_SetTrack3DPosition(free_track, &pos);
867 std::shared_ptr<MIX_Audio>
sound;
868 if(sound_cache.count(real_path) != 0) {
869 sound = sound_cache[real_path];
870 DBG_AUDIO <<
"cache hit for " << real_path;
872 sound.reset(MIX_LoadAudio(mixer, real_path.c_str(),
false), &MIX_DestroyAudio);
873 DBG_AUDIO <<
"cache miss for " << real_path;
876 MIX_SetTrackAudio(free_track,
sound.get());
881 if(loop_ticks > 0ms) {
882 if(fadein_ticks > 0ms) {
883 SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
884 SDL_SetNumberProperty(props, MIX_PROP_PLAY_MAX_MILLISECONDS_NUMBER, loop_ticks.count());
886 SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
889 if(fadein_ticks > 0ms) {
890 SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
892 SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, repeats);
896 res = MIX_PlayTrack(free_track, props);
899 ERR_AUDIO <<
"error playing sound effect " << real_path <<
" : " << SDL_GetError();
902 }
else if(group == sound_tracks::type::sound_source) {
905 std::scoped_lock lock(soundsource_map_mutex);
906 unsigned int* key =
const_cast<unsigned int*
>(&(soundsource_map.emplace(soundsource_id, free_track).first->first));
907 DBG_AUDIO <<
"adding callback for soundsource id " << *key;
908 MIX_SetTrackStoppedCallback(free_track, [](
void* userdata, MIX_Track*){
909 std::scoped_lock lock(soundsource_map_mutex);
910 DBG_AUDIO <<
"in callback to erase soundsource mapping for id " << *
static_cast<unsigned int*
>(userdata);
911 soundsource_map.erase(*
static_cast<unsigned int*
>(userdata));
915 if(res && sound_cache.count(real_path) == 0) {
916 sound_cache.emplace(real_path,
sound);
917 sound_cache_insertion_order.emplace_back(real_path);
919 if(sound_cache.size() > sound_cache_limit) {
920 std::string to_erase = sound_cache_insertion_order[0];
921 DBG_AUDIO <<
"Uncaching sound file " << to_erase;
922 sound_cache_insertion_order.erase(sound_cache_insertion_order.begin());
923 sound_cache.erase(to_erase);
933 sound::play_sound_internal(files, group, repeats);
940 sound::play_sound_internal(files, sound_tracks::type::sound_source, repeats, distance,
id);
948 sound::play_sound_internal(files, sound_tracks::type::sound_bell);
953 void play_timer(
const std::string& files,
const std::chrono::milliseconds& loop_ticks,
const std::chrono::milliseconds& fadein_ticks)
956 sound::play_sound_internal(files, sound_tracks::type::sound_timer, 0,
DISTANCE_NONE, UINT_MAX, loop_ticks, fadein_ticks);
964 sound::play_sound_internal(files, sound_tracks::type::sound_ui);
971 return MIX_SetTrackGain(sound::music_tracks[0], -1);
979 if(mix_ok && vol >= 0) {
984 MIX_SetTrackGain(sound::music_tracks[0], vol);
993 return MIX_GetTrackGain(sound::sound_source_tracks[0]);
1000 if(mix_ok && vol >= 0) {
1006 for(track_source&
t : sound_source_tracks) {
1007 MIX_SetTrackGain(
t, vol);
1010 for(track_source&
t : sound_fx_tracks) {
1011 MIX_SetTrackGain(
t, vol);
1021 if(mix_ok && vol >= 0) {
1026 MIX_SetTrackGain(sound::bell_tracks[0], vol);
1027 MIX_SetTrackGain(sound::timer_tracks[0], vol);
1033 if(mix_ok && vol >= 0) {
1038 for(track_source&
t : UI_sound_tracks) {
1039 MIX_SetTrackGain(
t, vol);
1046 music_cache.clear();
1047 music_cache_insertion_order.clear();
1048 sound_cache.clear();
1049 sound_cache_insertion_order.clear();
constexpr std::enable_if< C==dynamic_extent, span< T, detail::span_sub< E, O >::value > >::type subspan() const
A config object defines a single node in a WML file, with access to child nodes.
virtual void join_global()
unsigned int sample_rate()
std::size_t sound_buffer_size()
static rng & default_instance()
int get_random_int(int min, int max)
void handle_window_event(const SDL_Event &event) override
Internal representation of music tracks.
static std::shared_ptr< music_track > create(const config &cfg)
Declarations for File-IO.
std::string id
Text to match against addon_info.tags()
Standard logging facilities (interface).
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::vector< game_tip > shuffle(const std::vector< game_tip > &tips)
Shuffles the tips.
Audio output for sound and music.
void write_music_play_list(config &snapshot)
void reposition_sound(unsigned id, unsigned int distance)
void set_bell_volume(int vol)
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
void remove_track(unsigned int i)
unsigned int get_num_tracks()
void play_music_once(const std::string &file)
utils::optional< unsigned int > get_current_track_index()
void set_track(unsigned int i, const std::shared_ptr< music_track > &to)
std::vector< std::string > enumerate_drivers()
void play_sound(const std::string &files, sound_tracks::type group, unsigned int repeats)
bool is_sound_playing(int id)
void play_timer(const std::string &files, const std::chrono::milliseconds &loop_ticks, const std::chrono::milliseconds &fadein_ticks)
void play_sound_positioned(const std::string &files, int repeats, unsigned int distance, unsigned int id)
void commit_music_changes()
void play_track(unsigned int i)
void play_UI_sound(const std::string &files)
void set_previous_track(std::shared_ptr< music_track > track)
std::shared_ptr< music_track > get_previous_music_track()
void set_music_volume(int vol)
std::shared_ptr< music_track > get_track(unsigned int i)
std::shared_ptr< music_track > get_current_track()
std::string current_driver()
void set_UI_volume(int vol)
void set_sound_volume(int vol)
void play_bell(const std::string &files)
auto find(Container &container, const Value &value, const Projection &projection={})
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_
static lg::log_domain log_audio("audio")
std::string filename
Filename.
static driver_status query()
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
static map_location::direction n