28 #include <SDL3_mixer/SDL_mixer.h>
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)
44 std::vector<std::shared_ptr<MIX_Track>>
tracks;
47 std::map<std::string, std::shared_ptr<MIX_Audio>>
music_cache;
50 std::map<std::string, std::shared_ptr<MIX_Audio>>
sound_cache;
57 using namespace std::chrono_literals;
62 utils::optional<std::chrono::steady_clock::time_point> music_start_time;
64 bool want_new_music =
false;
65 auto fade_out_time = 5000ms;
66 bool no_fading =
false;
69 const std::size_t n_of_tracks = 32;
71 const std::size_t music_track_id = 0;
73 const std::size_t bell_track_id = 1;
74 const std::size_t timer_track_id = 2;
77 const std::size_t source_track_id_start = 3;
78 const std::size_t source_track_id_last = 10;
80 const std::size_t UI_sound_track_id_start = 11;
81 const std::size_t UI_sound_track_id_last = 12;
83 const std::size_t n_reserved_tracks_id_start = 13;
84 const std::size_t n_reserved_tracks_id_end = n_of_tracks;
86 const std::size_t music_cache_limit = 30;
87 const std::size_t sound_cache_limit = 500;
92 std::vector<std::string> played_before;
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;
107 std::vector<std::shared_ptr<sound::music_track>>::const_iterator find_track(
const sound::music_track& track)
110 [](
const std::shared_ptr<const sound::music_track>& ptr) {
return *ptr; });
119 if(current_track_index >= current_track_list.size()){
122 return current_track_index;
126 return current_track;
130 return previous_track;
134 previous_track = std::move(track);
139 return current_track_list.size();
144 if(
i < current_track_list.size()) {
145 return current_track_list[
i];
148 if(
i == current_track_list.size()) {
149 return current_track;
155 void set_track(
unsigned int i,
const std::shared_ptr<music_track>& to)
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);
164 if(
i >= current_track_list.size()) {
168 if(
i == current_track_index) {
171 current_track->set_play_once(
true);
174 current_track_index = current_track_list.size() - 1;
175 }
else if(
i < current_track_index) {
176 current_track_index--;
179 current_track_list.erase(current_track_list.begin() +
i);
194 if(
id == current_track->file_path()) {
198 if(current_track_list.size() <= 3) {
210 unsigned int num_played = 0;
211 std::set<std::string> played;
212 std::vector<std::string>::reverse_iterator
i;
214 for(
i = played_before.rbegin();
i != played_before.rend(); ++
i) {
217 if(num_played == 2) {
226 if(num_played == 2 && played.size() != current_track_list.size() - 1) {
227 LOG_AUDIO <<
"Played twice with only " << played.size() <<
" tracks between";
232 i = played_before.rbegin();
233 if(
i != played_before.rend()) {
235 if(
i != played_before.rend()) {
237 LOG_AUDIO <<
"Played just before previous";
248 assert(!current_track_list.empty());
250 if(current_track_index >= current_track_list.size()) {
251 current_track_index = 0;
254 if(current_track_list[current_track_index]->
shuffle()) {
255 unsigned int track = 0;
257 if(current_track_list.size() > 1) {
260 }
while(!
track_ok(current_track_list[track]->file_path()));
263 current_track_index = track;
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];
271 static std::string
pick_one(
const std::string& files)
279 if(ids.size() == 1) {
284 static std::map<std::string, unsigned int> prev_choices;
287 if(prev_choices.find(files) != prev_choices.end()) {
289 if(choice >= prev_choices[files]) {
293 prev_choices[files] = choice;
296 prev_choices.emplace(files, choice);
306 const char*
const drvname = SDL_GetCurrentAudioDriver();
307 return drvname ? drvname :
"<not initialized>";
312 std::vector<std::string> res;
313 int num_drivers = SDL_GetNumVideoDrivers();
315 for(
int n = 0;
n < num_drivers; ++
n) {
316 const char* drvname = SDL_GetAudioDriver(
n);
317 res.emplace_back(drvname ? drvname :
"<invalid driver>");
329 if(MIX_GetMixerFormat(
mixer, &spec)) {
331 res.frequency = spec.freq;
332 res.format = spec.format;
333 res.channels = spec.channels;
343 if(SDL_WasInit(SDL_INIT_AUDIO) == 0) {
344 if(!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
345 ERR_AUDIO <<
"Could not initialize audio: " << SDL_GetError();
353 ERR_AUDIO <<
"Could not initialize mixer: " << SDL_GetError();
360 spec.format = SDL_AUDIO_S16;
362 mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec);
365 ERR_AUDIO <<
"Could not initialize audio: " << SDL_GetError();
371 std::shared_ptr<MIX_Track>
music_track(MIX_CreateTrack(
mixer), &MIX_DestroyTrack);
372 MIX_TagTrack(
music_track.get(), sound_tracks::music);
374 track_map.emplace(music_track_id, sound_tracks::type::music);
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);
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);
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);
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);
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);
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";
440 MIX_DestroyMixer(
mixer);
451 if(SDL_WasInit(SDL_INIT_AUDIO) != 0) {
452 SDL_QuitSubSystem(SDL_INIT_AUDIO);
465 if(music ||
sound || bell || UI_sound) {
490 MIX_StopTrack(
tracks[0].
get(), MIX_TrackMSToFrames(
tracks[music_track_id].
get(), 500));
497 MIX_StopTag(
mixer, sound_tracks::sound_source, 0);
498 MIX_StopTag(
mixer, sound_tracks::sound_fx, 0);
508 MIX_StopTag(
mixer, sound_tracks::sound_bell, 0);
509 MIX_StopTag(
mixer, sound_tracks::sound_timer, 0);
516 MIX_StopTag(
mixer, sound_tracks::sound_ui, 0);
524 current_track = std::move(track);
525 current_track->set_play_once(
true);
526 current_track_index = current_track_list.size();
533 current_track_list.clear();
542 music_start_time = std::chrono::steady_clock::now();
543 want_new_music =
true;
545 fade_out_time = previous_track !=
nullptr ? previous_track->ms_after() : 0ms;
551 if(
i >= current_track_list.size()) {
554 current_track_index =
i;
555 current_track = current_track_list[
i];
562 music_start_time.reset();
563 want_new_music =
true;
569 std::string
filename = current_track->file_path();
575 auto fading_time = current_track->ms_before();
585 MIX_StopTrack(
tracks[music_track_id].
get(), 0);
587 std::shared_ptr<MIX_Audio> music;
592 music.reset(MIX_LoadAudio(
mixer,
filename.c_str(),
false), &MIX_DestroyAudio);
597 MIX_SetTrackAudio(
tracks[music_track_id].
get(), music.get());
600 SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fading_time.count());
602 if(!MIX_PlayTrack(
tracks[music_track_id].
get(), props)) {
603 ERR_AUDIO <<
"Could not play music: " << SDL_GetError() <<
" " <<
filename <<
" ";
610 DBG_AUDIO <<
"Uncaching music file " << to_erase;
616 want_new_music =
false;
630 ERR_AUDIO <<
"cannot open track; disabled in this playlist.";
637 current_track = std::move(track);
638 current_track_index = current_track_list.size();
645 current_track_list.clear();
648 auto iter = find_track(*track);
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();
658 iter = current_track_list.insert(insert_at, track);
659 auto new_track_index = std::distance(current_track_list.cbegin(), iter);
663 if(new_track_index <= current_track_index) {
664 ++current_track_index;
673 current_track = *iter;
674 current_track_index = std::distance(current_track_list.cbegin(), iter);
676 }
else if(!track->
append() && !allow_interrupt_current_track && current_track) {
678 current_track->set_play_once(
true);
686 auto now = std::chrono::steady_clock::now();
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) {
694 music_start_time = now;
699 if(music_start_time && music_refresh_rate.poll()) {
700 want_new_music = now >= *music_start_time - fade_out_time;
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()));
714 music_muter::music_muter()
715 :
events::sdl_handler(false)
723 if(event.type == SDL_EVENT_WINDOW_FOCUS_GAINED) {
724 MIX_ResumeTrack(
tracks[music_track_id].
get());
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());
737 played_before.clear();
741 if(current_track->play_once()) {
746 for(
auto m : current_track_list) {
747 if(*current_track == *m) {
754 if(current_track_list.empty()) {
768 for(
auto m : current_track_list) {
769 m->write(snapshot, append);
776 for(
unsigned ch = 0; ch <
tracks.size(); ++ch) {
788 MIX_SetTrack3DPosition(
tracks[ch].
get(), &pos);
795 return MIX_TrackPlaying(
tracks[
id].
get());
803 using namespace std::chrono_literals;
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)
813 if(files.empty() || !mix_ok) {
817 if(group == sound_tracks::type::sound_source) {
818 if(soundsource_id != UINT_MAX) {
831 if(track.second == group && !MIX_TrackPlaying(
tracks[track.first].get())) {
832 free_track = track.first;
836 if(free_track == -1) {
840 DBG_AUDIO <<
"playing " << files <<
" on track " << free_track;
845 std::string real_path = localized.value_or(
filename.value());
851 MIX_SetTrack3DPosition(
tracks[free_track].
get(), &pos);
853 std::shared_ptr<MIX_Audio>
sound;
856 DBG_AUDIO <<
"cache hit for " << real_path;
858 sound.reset(MIX_LoadAudio(
mixer, real_path.c_str(),
false), &MIX_DestroyAudio);
859 DBG_AUDIO <<
"cache miss for " << real_path;
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());
872 SDL_SetNumberProperty(props.
id(), MIX_PROP_PLAY_LOOPS_NUMBER, -1);
875 if(fadein_ticks > 0ms) {
876 SDL_SetNumberProperty(props.
id(), MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
878 SDL_SetNumberProperty(props.
id(), MIX_PROP_PLAY_LOOPS_NUMBER, repeats);
882 res = MIX_PlayTrack(
tracks[free_track].
get(), props.
id());
885 ERR_AUDIO <<
"error playing sound effect " << real_path <<
" : " << SDL_GetError();
888 }
else if(group == sound_tracks::type::sound_source) {
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*){
896 DBG_AUDIO <<
"in callback to erase soundsource mapping for id " << *
static_cast<unsigned int*
>(userdata);
907 DBG_AUDIO <<
"Uncaching sound file " << to_erase;
940 void play_timer(
const std::string& files,
const std::chrono::milliseconds& loop_ticks,
const std::chrono::milliseconds& fadein_ticks)
966 if(mix_ok && vol >= 0) {
986 if(mix_ok && vol >= 0) {
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) {
1005 if(mix_ok && vol >= 0) {
1017 if(mix_ok && vol >= 0) {
1022 for(
unsigned i = UI_sound_track_id_start;
i <= UI_sound_track_id_last; ++
i) {
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)
SDL_PropertiesID id() const
void handle_window_event(const SDL_Event &event) override
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::string id
Text to match against addon_info.tags()
Standard logging facilities (interface).
static lua_music_track * get_track(lua_State *L, int i)
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.
static int music_volume()
static bool ui_sound_on()
static int sound_volume()
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)
std::vector< std::shared_ptr< MIX_Track > > tracks
void set_bell_volume(int vol)
std::mutex soundsource_map_mutex
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()
std::map< unsigned int, int > soundsource_map
void stop_sound(unsigned id)
std::map< int, sound_tracks::type > track_map
std::vector< std::string > sound_cache_insertion_order
void play_music_once(const std::string &file)
std::map< std::string, std::shared_ptr< MIX_Audio > > sound_cache
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)
std::size_t mixer_init_counter
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)
std::map< std::string, std::shared_ptr< MIX_Audio > > music_cache
void play_sound_positioned(const std::string &files, int repeats, unsigned int distance, unsigned int id)
void commit_music_changes()
static void play_new_music()
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_current_track()
std::string current_driver()
std::vector< std::string > music_cache_insertion_order
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)
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.
static std::shared_ptr< sound::music_track > choose_track()
static lg::log_domain log_audio("audio")
static bool track_ok(const std::string &id)
static std::string pick_one(const std::string &files)
std::string filename
Filename.
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
static map_location::direction n