27 #include <SDL3_mixer/SDL_mixer.h>
32 #include <unordered_map>
36 #define DBG_AUDIO LOG_STREAM(debug, log_audio)
37 #define LOG_AUDIO LOG_STREAM(info, log_audio)
38 #define ERR_AUDIO LOG_STREAM(err, log_audio)
42 template<
typename Container,
typename Value>
45 auto iter = map.find(
value);
46 if(iter == map.end()) {
47 return utils::nullopt;
64 void allocate_channel(MIX_Mixer* mixer,
const char* type_tag)
66 track_.reset(MIX_CreateTrack(mixer));
67 MIX_TagTrack(*
this, type_tag);
71 operator MIX_Track*()
const
77 std::unique_ptr<MIX_Track, decltype(&MIX_DestroyTrack)>
track_{
nullptr, &MIX_DestroyTrack};
80 std::mutex soundsource_map_mutex;
81 std::map<unsigned int, MIX_Track*> soundsource_map;
83 std::array<channel, 32> channel_pool{};
98 explicit audio_cache(std::size_t
size)
103 MIX_Audio* get_or_insert(MIX_Mixer* mixer,
const std::string&
filename)
107 cached_audio->last_access = cache_item::clock::now();
108 return cached_audio->value.get();
112 MIX_Audio* audio = MIX_LoadAudio(mixer,
filename.data(),
false);
115 auto to_erase = least_recently_used();
116 DBG_AUDIO <<
"Dropping music file from cache: " << to_erase->first;
117 cache.erase(to_erase);
132 explicit cache_item(MIX_Audio* ptr)
133 :
value(ptr, &MIX_DestroyAudio)
138 std::unique_ptr<MIX_Audio, decltype(&MIX_DestroyAudio)>
value;
140 using clock = std::chrono::steady_clock;
145 std::unordered_map<std::string, cache_item>
cache;
147 auto least_recently_used() const -> decltype(
cache)::const_iterator
149 #ifdef __cpp_lib_ranges
150 return std::ranges::min_element(
cache, {},
151 [](
const auto&
value) {
return value.second.last_access; });
153 return std::min_element(
cache.begin(),
cache.end(),
154 [](
const auto& lhs,
const auto& rhs) { return lhs.second.last_access < rhs.second.last_access; });
159 constexpr std::size_t music_cache_limit = 30;
160 constexpr std::size_t sound_cache_limit = 500;
162 audio_cache music_cache{music_cache_limit};
163 audio_cache sound_cache{sound_cache_limit};
165 MIX_Mixer* mixer =
nullptr;
166 std::size_t mixer_init_counter = 0;
168 using namespace std::chrono_literals;
170 utils::optional<std::chrono::steady_clock::time_point> music_start_time;
172 bool want_new_music =
false;
173 auto fade_out_time = 5000ms;
174 bool no_fading =
false;
176 std::vector<std::string> played_before;
186 std::vector<std::shared_ptr<sound::music_track>> current_track_list;
187 std::shared_ptr<sound::music_track> current_track;
188 std::shared_ptr<sound::music_track> previous_track;
190 std::vector<std::shared_ptr<sound::music_track>>::const_iterator find_track(
const sound::music_track& track)
193 [](
const std::shared_ptr<const sound::music_track>& ptr) {
return *ptr; });
201 return utils::nullopt;
206 if(iter == current_track_list.end()) {
207 return utils::nullopt;
210 return std::distance(current_track_list.begin(), iter);
214 return current_track;
218 previous_track = std::exchange(current_track, std::move(track));
222 return previous_track;
227 return current_track_list.size();
232 if(
i < current_track_list.size()) {
233 return current_track_list[
i];
236 if(
i == current_track_list.size()) {
237 return current_track;
243 void set_track(
unsigned int i,
const std::shared_ptr<music_track>& to)
245 if(
i < current_track_list.size() && find_track(*to) != current_track_list.end()) {
246 current_track_list[
i] = std::make_shared<music_track>(*to);
252 if(
i >= current_track_list.size()) {
257 if(current_track && current_track == current_track_list[
i]) {
258 current_track->set_play_once(
true);
261 current_track_list.erase(current_track_list.begin() +
i);
266 bool track_ok(
const std::string&
id)
276 if(
id == current_track->file_path()) {
280 if(current_track_list.size() <= 3) {
292 unsigned int num_played = 0;
293 std::set<std::string> played;
294 std::vector<std::string>::reverse_iterator
i;
296 for(
i = played_before.rbegin();
i != played_before.rend(); ++
i) {
299 if(num_played == 2) {
308 if(num_played == 2 && played.size() != current_track_list.size() - 1) {
309 LOG_AUDIO <<
"Played twice with only " << played.size() <<
" tracks between";
314 i = played_before.rbegin();
315 if(
i != played_before.rend()) {
317 if(
i != played_before.rend()) {
319 LOG_AUDIO <<
"Played just before previous";
328 std::shared_ptr<sound::music_track> choose_track()
330 assert(!current_track_list.empty());
333 std::size_t next_index{0};
341 if(!current_index || current_track->shuffle()) {
342 if(current_track_list.size() > 1) {
345 }
while(!track_ok(current_track_list[next_index]->file_path()));
348 next_index = (current_index.value() + 1) % current_track_list.size();
351 std::shared_ptr next_track = current_track_list[next_index];
352 DBG_AUDIO <<
"Next track will be " << next_track->file_path();
353 played_before.push_back(current_track->file_path());
357 std::string pick_one(
const std::string& files)
365 if(ids.size() == 1) {
370 static std::map<std::string, unsigned int> prev_choices;
373 if(prev_choices.find(files) != prev_choices.end()) {
375 if(choice >= prev_choices[files]) {
379 prev_choices[files] = choice;
382 prev_choices.emplace(files, choice);
392 const char*
const drvname = SDL_GetCurrentAudioDriver();
393 return drvname ? drvname :
"<not initialized>";
398 std::vector<std::string> res;
399 int num_drivers = SDL_GetNumVideoDrivers();
401 for(
int n = 0;
n < num_drivers; ++
n) {
402 const char* drvname = SDL_GetAudioDriver(
n);
403 res.emplace_back(drvname ? drvname :
"<invalid driver>");
413 if(MIX_GetMixerFormat(mixer, &spec)) {
429 if(SDL_WasInit(SDL_INIT_AUDIO) == 0) {
430 if(!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
431 ERR_AUDIO <<
"Could not initialize audio: " << SDL_GetError();
437 mixer_init_counter++;
439 ERR_AUDIO <<
"Could not initialize mixer: " << SDL_GetError();
446 spec.format = SDL_AUDIO_S16;
448 mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec);
450 ERR_AUDIO <<
"Could not initialize audio: " << SDL_GetError();
455 c.allocate_channel(mixer, sound_tracks::music);
459 c.allocate_channel(mixer, sound_tracks::sound_bell);
463 c.allocate_channel(mixer, sound_tracks::sound_timer);
466 for(
channel&
c : positional_channels) {
467 c.allocate_channel(mixer, sound_tracks::sound_source);
471 c.allocate_channel(mixer, sound_tracks::sound_ui);
475 c.allocate_channel(mixer, sound_tracks::sound_fx);
492 MIX_StopAllTracks(mixer, 0);
495 MIX_DestroyMixer(mixer);
504 while(mixer_init_counter-- > 0) {
508 if(SDL_WasInit(SDL_INIT_AUDIO) != 0) {
509 SDL_QuitSubSystem(SDL_INIT_AUDIO);
518 MIX_StopTag(mixer, sound_tracks::music, 500);
525 MIX_StopTag(mixer, sound_tracks::sound_source, 0);
526 MIX_StopTag(mixer, sound_tracks::sound_fx, 0);
536 MIX_StopTag(mixer, sound_tracks::sound_bell, 0);
537 MIX_StopTag(mixer, sound_tracks::sound_timer, 0);
544 MIX_StopTag(mixer, sound_tracks::sound_ui, 0);
551 MIX_ResumeTag(mixer, sound_tracks::music);
558 MIX_ResumeTag(mixer, sound_tracks::sound_source);
559 MIX_ResumeTag(mixer, sound_tracks::sound_fx);
566 MIX_ResumeTag(mixer, sound_tracks::sound_bell);
567 MIX_ResumeTag(mixer, sound_tracks::sound_timer);
574 MIX_ResumeTag(mixer, sound_tracks::sound_ui);
582 current_track->set_play_once(
true);
589 current_track_list.clear();
598 music_start_time = std::chrono::steady_clock::now();
599 want_new_music =
true;
601 fade_out_time = previous_track !=
nullptr ? previous_track->ms_after() : 0ms;
606 if(
i >= current_track_list.size()) {
616 void play_new_music()
618 music_start_time.reset();
619 want_new_music =
true;
621 if(!
prefs::get().music_on() || !mixer || !current_track) {
625 std::string
filename = current_track->file_path();
631 auto fading_time = current_track->ms_before();
641 MIX_StopTrack(music_channels[0], 0);
643 MIX_Audio* music = music_cache.get_or_insert(mixer,
filename);
644 MIX_SetTrackAudio(music_channels[0], music);
648 SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fading_time.count());
650 if(!MIX_PlayTrack(music_channels[0], props)) {
651 ERR_AUDIO <<
"Could not play music: " << SDL_GetError() <<
" " <<
filename <<
" ";
654 want_new_music =
false;
657 MIX_Track* get_positional_channel(
unsigned soundsource_id)
659 std::scoped_lock lock{soundsource_map_mutex};
660 const auto it = soundsource_map.find(soundsource_id);
661 return it == soundsource_map.end() ? nullptr : it->second;
677 ERR_AUDIO <<
"cannot open track; disabled in this playlist.";
690 current_track_list.clear();
693 auto iter = find_track(*track);
697 if(iter == current_track_list.end()) {
698 auto insert_at = (
i >= 0 &&
static_cast<std::size_t
>(
i) < current_track_list.size())
699 ? current_track_list.begin() +
i
700 : current_track_list.end();
703 iter = current_track_list.insert(insert_at, track);
712 }
else if(!track->
append() && !allow_interrupt_current_track && current_track) {
714 current_track->set_play_once(
true);
720 if(MIX_GetTrackFadeFrames(music_channels[0]) != 0) {
727 auto now = std::chrono::steady_clock::now();
729 bool is_playing = MIX_TrackPlaying(music_channels[0]);
730 bool is_paused = MIX_TrackPaused(music_channels[0]);
731 if(!music_start_time && !current_track_list.empty() && !is_playing && !is_paused) {
734 music_start_time = now;
739 if(music_start_time && music_refresh_rate.poll()) {
740 want_new_music = now >= *music_start_time - fade_out_time;
744 if(MIX_TrackPlaying(music_channels[0])) {
745 MIX_StopTrack(music_channels[0], MIX_TrackMSToFrames(music_channels[0], fade_out_time.count()));
755 :
events::sdl_handler(false)
763 if(event.type == SDL_EVENT_WINDOW_FOCUS_GAINED) {
764 MIX_ResumeTrack(music_channels[0]);
766 }
else if(event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
767 if(MIX_TrackPlaying(music_channels[0])) {
768 MIX_PauseTrack(music_channels[0]);
777 played_before.clear();
781 if(current_track->play_once()) {
786 for(
auto m : current_track_list) {
787 if(*current_track == *m) {
794 if(current_track_list.empty()) {
807 for(
auto m : current_track_list) {
808 m->write(snapshot, append);
815 if(MIX_Track* track = get_positional_channel(
id)) {
817 MIX_StopTrack(track, 0);
823 MIX_SetTrack3DPosition(track, &pos);
830 MIX_Track* track = get_positional_channel(
id);
831 return track && MIX_TrackPlaying(track);
843 const auto search = [](
const auto& span) -> MIX_Track* {
845 if(!MIX_TrackPlaying(
c)) {
854 case sound_tracks::type::music:
855 return search(music_channels);
856 case sound_tracks::type::sound_bell:
857 return search(bell_channels);
858 case sound_tracks::type::sound_timer:
859 return search(timer_channels);
860 case sound_tracks::type::sound_source:
861 return search(positional_channels);
862 case sound_tracks::type::sound_ui:
863 return search(UI_channels);
864 case sound_tracks::type::sound_fx:
865 return search(SFX_channels);
871 void play_sound_internal(
const std::string& files,
873 unsigned int repeats = 0,
874 unsigned int distance = 0,
875 unsigned int soundsource_id = UINT_MAX,
876 const std::chrono::milliseconds& loop_ticks = 0ms,
877 const std::chrono::milliseconds& fadein_ticks = 0ms)
879 if(files.empty() || !mixer) {
883 if(group == sound_tracks::type::sound_source) {
884 if(soundsource_id != UINT_MAX) {
894 MIX_Track* free_channel = find_free_channel(group);
900 std::string file = pick_one(files);
903 ERR_AUDIO <<
"Could not locate sound file '" << file <<
"'.";
907 std::string real_path = localized.value_or(
filename.value());
913 MIX_SetTrack3DPosition(free_channel, &pos);
915 MIX_Audio*
sound = sound_cache.get_or_insert(mixer, real_path);
916 MIX_SetTrackAudio(free_channel,
sound);
919 if(loop_ticks > 0ms) {
920 SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
921 if(fadein_ticks > 0ms) {
922 SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
923 SDL_SetNumberProperty(props, MIX_PROP_PLAY_MAX_MILLISECONDS_NUMBER, loop_ticks.count());
926 if(fadein_ticks > 0ms) {
927 SDL_SetNumberProperty(props, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, fadein_ticks.count());
929 SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, repeats);
933 if(!MIX_PlayTrack(free_channel, props)) {
934 ERR_AUDIO <<
"error playing sound effect " << real_path <<
" : " << SDL_GetError();
936 }
else if(group == sound_tracks::type::sound_source) {
939 std::scoped_lock lock(soundsource_map_mutex);
940 unsigned int* key =
const_cast<unsigned int*
>(&(soundsource_map.emplace(soundsource_id, free_channel).first->first));
941 DBG_AUDIO <<
"adding callback for soundsource id " << *key;
942 MIX_SetTrackStoppedCallback(free_channel, [](
void* userdata, MIX_Track*){
943 std::scoped_lock lock(soundsource_map_mutex);
944 DBG_AUDIO <<
"in callback to erase soundsource mapping for id " << *
static_cast<unsigned int*
>(userdata);
945 soundsource_map.erase(*
static_cast<unsigned int*
>(userdata));
951 volume clamp_gain(volume
value)
961 sound::play_sound_internal(files, group, repeats);
968 sound::play_sound_internal(files, sound_tracks::type::sound_source, repeats, distance,
id);
976 sound::play_sound_internal(files, sound_tracks::type::sound_bell);
981 void play_timer(
const std::string& files,
const std::chrono::milliseconds& loop_ticks,
const std::chrono::milliseconds& fadein_ticks)
984 sound::play_sound_internal(files, sound_tracks::type::sound_timer, 0,
distance_none, UINT_MAX, loop_ticks, fadein_ticks);
992 sound::play_sound_internal(files, sound_tracks::type::sound_ui);
999 return volume{MIX_GetTrackGain(sound::music_channels[0])};
1008 MIX_SetTagGain(mixer, sound_tracks::music, clamp_gain(vol));
1016 return volume{MIX_GetTrackGain(sound::positional_channels[0])};
1025 vol = clamp_gain(vol);
1027 MIX_SetTagGain(mixer, sound_tracks::sound_source, vol);
1028 MIX_SetTagGain(mixer, sound_tracks::sound_fx, vol);
1038 vol = clamp_gain(vol);
1040 MIX_SetTagGain(mixer, sound_tracks::sound_bell, vol);
1041 MIX_SetTagGain(mixer, sound_tracks::sound_timer, vol);
1048 MIX_SetTagGain(mixer, sound_tracks::sound_ui, clamp_gain(vol));
1054 music_cache.clear();
1055 sound_cache.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()
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.
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()
Standard logging facilities (interface).
void clear()
Clear the current render target.
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)
void reposition_sound(unsigned id, unsigned int distance)
void set_UI_volume(volume vol)
void set_music_volume(volume vol)
void set_current_track(std::shared_ptr< music_track > track)
volume get_music_volume()
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
void remove_track(unsigned int i)
utils::optional< std::size_t > get_current_track_index()
unsigned int get_num_tracks()
constexpr int distance_none
void set_sound_volume(volume vol)
void play_music_once(const std::string &file)
constexpr int distance_silent
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)
void set_bell_volume(volume vol)
bool is_sound_playing(int id)
volume get_sound_volume()
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)
constexpr volume max_volume
void play_UI_sound(const std::string &files)
std::shared_ptr< music_track > get_previous_music_track()
std::shared_ptr< music_track > get_track(unsigned int i)
std::shared_ptr< music_track > get_current_track()
std::string current_driver()
void play_bell(const std::string &files)
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
auto find(Container &container, const Value &value, const Projection &projection={})
auto find_in(Container &map, const Value &value) -> utils::optional_reference< typename Container::mapped_type >
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_
std::unordered_map< std::string, cache_item > cache
const std::size_t max_size
static lg::log_domain log_audio("audio")
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
clock::time_point last_access
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