mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-03 21:39:44 +00:00
936 lines
24 KiB
C++
936 lines
24 KiB
C++
/*
|
|
Copyright (C) 2003 - 2014 by David White <dave@whitevine.net>
|
|
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY.
|
|
|
|
See the COPYING file for more details.
|
|
*/
|
|
|
|
#include "global.hpp"
|
|
|
|
#include "config.hpp"
|
|
#include "filesystem.hpp"
|
|
#include "game_preferences.hpp"
|
|
#include "log.hpp"
|
|
#include "serialization/string_utils.hpp"
|
|
#include "sound.hpp"
|
|
#include "sound_music_track.hpp"
|
|
#include "util.hpp"
|
|
|
|
#include "SDL_mixer.h"
|
|
#include "SDL.h" // Travis doesn't like this, although it works on my machine -> '#include "SDL_sound.h"
|
|
|
|
#include <boost/foreach.hpp>
|
|
|
|
#include <list>
|
|
#include <string>
|
|
#include <sstream>
|
|
|
|
static lg::log_domain log_audio("audio");
|
|
#define DBG_AUDIO LOG_STREAM(debug, log_audio)
|
|
#define LOG_AUDIO LOG_STREAM(info, log_audio)
|
|
#define ERR_AUDIO LOG_STREAM(err, log_audio)
|
|
|
|
|
|
#if (MIX_MAJOR_VERSION < 1) || (MIX_MAJOR_VERSION == 1) && ((MIX_MINOR_VERSION < 2) || (MIX_MINOR_VERSION == 2) && (MIX_PATCHLEVEL <= 11))
|
|
#define SDL_MIXER_OLD_VERSION
|
|
#endif
|
|
|
|
namespace sound {
|
|
// Channel-chunk mapping lets us know, if we can safely free a given chunk
|
|
static std::vector<Mix_Chunk*> channel_chunks;
|
|
|
|
// Channel-id mapping for use with sound sources (to check if given source
|
|
// is playing on a channel for fading/panning)
|
|
static std::vector<int> channel_ids;
|
|
|
|
static void play_sound_internal(const std::string& files, channel_group group, unsigned int repeats=0,
|
|
unsigned int distance=0, int id=-1, int loop_ticks=0, int fadein_ticks=0);
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool mix_ok = false;
|
|
int music_start_time = 0;
|
|
unsigned music_refresh = 0;
|
|
unsigned music_refresh_rate = 20;
|
|
bool want_new_music = false;
|
|
int fadingout_time=5000;
|
|
bool no_fading = false;
|
|
|
|
// number of allocated channels,
|
|
const size_t n_of_channels = 32;
|
|
|
|
// we need 2 channels, because we it for timer as well
|
|
const size_t bell_channel = 0;
|
|
const size_t timer_channel = 1;
|
|
|
|
// number of channels reserved for sound sources
|
|
const size_t source_channels = 8;
|
|
const size_t source_channel_start = timer_channel + 1;
|
|
const size_t source_channel_last = source_channel_start + source_channels - 1;
|
|
const size_t UI_sound_channels = 2;
|
|
const size_t UI_sound_channel_start = source_channel_last + 1;
|
|
const size_t UI_sound_channel_last = UI_sound_channel_start + UI_sound_channels - 1;
|
|
const size_t n_reserved_channels = UI_sound_channel_last + 1; // sources, bell, timer and UI
|
|
|
|
// Max number of sound chunks that we want to cache
|
|
// Keep this above number of available channels to avoid busy-looping
|
|
#ifdef LOW_MEM
|
|
unsigned max_cached_chunks = 64;
|
|
#else
|
|
unsigned max_cached_chunks = 256;
|
|
#endif
|
|
|
|
std::map< Mix_Chunk*, int > chunk_usage;
|
|
|
|
}
|
|
|
|
static void increment_chunk_usage(Mix_Chunk* mcp) {
|
|
++(chunk_usage[mcp]);
|
|
}
|
|
|
|
static void decrement_chunk_usage(Mix_Chunk* mcp) {
|
|
if(mcp == NULL) return;
|
|
std::map< Mix_Chunk*, int >::iterator this_usage = chunk_usage.find(mcp);
|
|
assert(this_usage != chunk_usage.end());
|
|
if(--(this_usage->second) == 0) {
|
|
Mix_FreeChunk(mcp);
|
|
chunk_usage.erase(this_usage);
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
class sound_cache_chunk {
|
|
public:
|
|
sound_cache_chunk(const std::string& f) : group(sound::NULL_CHANNEL), file(f), data_(NULL) {}
|
|
sound_cache_chunk(const sound_cache_chunk& scc)
|
|
: group(scc.group), file(scc.file), data_(scc.data_)
|
|
{
|
|
increment_chunk_usage(data_);
|
|
}
|
|
|
|
~sound_cache_chunk()
|
|
{
|
|
decrement_chunk_usage(data_);
|
|
}
|
|
|
|
sound::channel_group group;
|
|
std::string file;
|
|
|
|
void set_data(Mix_Chunk* d) {
|
|
increment_chunk_usage(d);
|
|
decrement_chunk_usage(data_);
|
|
data_ = d;
|
|
}
|
|
|
|
Mix_Chunk* get_data() const {
|
|
return data_;
|
|
}
|
|
|
|
bool operator==(sound_cache_chunk const &scc) const {
|
|
return file == scc.file;
|
|
}
|
|
|
|
bool operator!=(sound_cache_chunk const &scc) const { return !operator==(scc); }
|
|
|
|
sound_cache_chunk& operator=(const sound_cache_chunk& scc) {
|
|
file = scc.file;
|
|
group = scc.group;
|
|
set_data(scc.get_data());
|
|
return *this;
|
|
}
|
|
|
|
private:
|
|
Mix_Chunk* data_;
|
|
};
|
|
|
|
std::list< sound_cache_chunk > sound_cache;
|
|
typedef std::list< sound_cache_chunk >::iterator sound_cache_iterator;
|
|
std::map<std::string,Mix_Music*> music_cache;
|
|
|
|
std::vector<std::string> played_before;
|
|
|
|
//
|
|
// FIXME: the first music_track may be initialized before main()
|
|
// is reached. Using the logging facilities may lead to a SIGSEGV
|
|
// because it's not guaranteed that their objects are already alive.
|
|
//
|
|
// Use the music_track default constructor to avoid trying to
|
|
// invoke a log object while resolving paths.
|
|
//
|
|
std::vector<sound::music_track> current_track_list;
|
|
sound::music_track current_track;
|
|
sound::music_track last_track;
|
|
unsigned int current_track_index = 0;
|
|
|
|
}
|
|
|
|
static bool track_ok(const std::string& id)
|
|
{
|
|
LOG_AUDIO << "Considering " << id << "\n";
|
|
|
|
// If they committed changes to list, we forget previous plays, but
|
|
// still *never* repeat same track twice if we have an option.
|
|
if (id == current_track.file_path())
|
|
return false;
|
|
|
|
if (current_track_list.size() <= 3)
|
|
return true;
|
|
|
|
// Timothy Pinkham says:
|
|
// 1) can't be repeated without 2 other pieces have already played
|
|
// since A was played.
|
|
// 2) cannot play more than 2 times without every other piece
|
|
// having played at least 1 time.
|
|
|
|
// Dammit, if our musicians keep coming up with algorithms, I'll
|
|
// be out of a job!
|
|
unsigned int num_played = 0;
|
|
std::set<std::string> played;
|
|
std::vector<std::string>::reverse_iterator i;
|
|
|
|
for (i = played_before.rbegin(); i != played_before.rend(); ++i) {
|
|
if (*i == id) {
|
|
++num_played;
|
|
if (num_played == 2)
|
|
break;
|
|
} else {
|
|
played.insert(*i);
|
|
}
|
|
}
|
|
|
|
// If we've played this twice, must have played every other track.
|
|
if (num_played == 2 && played.size() != current_track_list.size() - 1) {
|
|
LOG_AUDIO << "Played twice with only " << played.size()
|
|
<< " tracks between\n";
|
|
return false;
|
|
}
|
|
|
|
// Check previous previous track not same.
|
|
i = played_before.rbegin();
|
|
if (i != played_before.rend()) {
|
|
++i;
|
|
if (i != played_before.rend()) {
|
|
if (*i == id) {
|
|
LOG_AUDIO << "Played just before previous\n";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
static const sound::music_track &choose_track()
|
|
{
|
|
assert(!current_track_list.empty());
|
|
|
|
if (current_track_index >= current_track_list.size()) {
|
|
current_track_index = 0;
|
|
}
|
|
|
|
if (current_track_list[current_track_index].shuffle()) {
|
|
unsigned int track = 0;
|
|
|
|
if (current_track_list.size() > 1) {
|
|
do {
|
|
track = rand()%current_track_list.size();
|
|
} while (!track_ok( current_track_list[track].file_path() ));
|
|
}
|
|
|
|
current_track_index = track;
|
|
}
|
|
|
|
//LOG_AUDIO << "Next track will be " << current_track_list[track].file_path() << "\n";
|
|
played_before.push_back( current_track_list[current_track_index].file_path() );
|
|
return current_track_list[current_track_index++];
|
|
}
|
|
|
|
static std::string pick_one(const std::string &files)
|
|
{
|
|
std::vector<std::string> ids = utils::square_parenthetical_split(files,',',"[","]");
|
|
|
|
if (ids.empty())
|
|
return "";
|
|
if (ids.size() == 1)
|
|
return ids[0];
|
|
|
|
#ifdef LOW_MEM
|
|
// We're memory constrained, so we shouldn't cache too many chunks
|
|
return ids[0];
|
|
#endif
|
|
|
|
// We avoid returning same choice twice if we can avoid it.
|
|
static std::map<std::string,unsigned int> prev_choices;
|
|
unsigned int choice;
|
|
|
|
if (prev_choices.find(files) != prev_choices.end()) {
|
|
choice = rand()%(ids.size()-1);
|
|
if (choice >= prev_choices[files])
|
|
++choice;
|
|
prev_choices[files] = choice;
|
|
} else {
|
|
choice = rand()%ids.size();
|
|
prev_choices.insert(std::pair<std::string,unsigned int>(files,choice));
|
|
}
|
|
|
|
return ids[choice];
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct audio_lock
|
|
{
|
|
audio_lock()
|
|
{
|
|
SDL_LockAudio();
|
|
}
|
|
|
|
~audio_lock()
|
|
{
|
|
SDL_UnlockAudio();
|
|
}
|
|
};
|
|
|
|
} // end of anonymous namespace
|
|
|
|
|
|
namespace sound {
|
|
|
|
// Removes channel-chunk and channel-id mapping
|
|
static void channel_finished_hook(int channel)
|
|
{
|
|
channel_chunks[channel] = NULL;
|
|
channel_ids[channel] = -1;
|
|
}
|
|
|
|
bool init_sound() {
|
|
LOG_AUDIO << "Initializing audio...\n";
|
|
if(SDL_WasInit(SDL_INIT_AUDIO) == 0)
|
|
if(SDL_InitSubSystem(SDL_INIT_AUDIO) == -1)
|
|
return false;
|
|
|
|
if(!mix_ok) {
|
|
if(Mix_OpenAudio(preferences::sample_rate(), MIX_DEFAULT_FORMAT, 2, preferences::sound_buffer_size()) == -1) {
|
|
mix_ok = false;
|
|
ERR_AUDIO << "Could not initialize audio: " << Mix_GetError() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
mix_ok = true;
|
|
Mix_AllocateChannels(n_of_channels);
|
|
Mix_ReserveChannels(n_reserved_channels);
|
|
|
|
channel_chunks.clear();
|
|
channel_chunks.resize(n_of_channels, NULL);
|
|
channel_ids.resize(n_of_channels, -1);
|
|
|
|
Mix_GroupChannel(bell_channel, SOUND_BELL);
|
|
Mix_GroupChannel(timer_channel, SOUND_TIMER);
|
|
Mix_GroupChannels(source_channel_start, source_channel_last, SOUND_SOURCES);
|
|
Mix_GroupChannels(UI_sound_channel_start, UI_sound_channel_last, SOUND_UI);
|
|
Mix_GroupChannels(n_reserved_channels, n_of_channels - 1, SOUND_FX);
|
|
|
|
set_sound_volume(preferences::sound_volume());
|
|
set_UI_volume(preferences::UI_volume());
|
|
set_music_volume(preferences::music_volume());
|
|
set_bell_volume(preferences::bell_volume());
|
|
|
|
Mix_ChannelFinished(channel_finished_hook);
|
|
|
|
LOG_AUDIO << "Audio initialized.\n";
|
|
|
|
DBG_AUDIO << "Channel layout: " << n_of_channels << " channels (" << n_reserved_channels << " reserved)\n"
|
|
<< " " << bell_channel << " - bell\n"
|
|
<< " " << timer_channel << " - timer\n"
|
|
<< " " << source_channel_start << ".." << source_channel_last << " - sound sources\n"
|
|
<< " " << UI_sound_channel_start << ".." << UI_sound_channel_last << " - UI\n"
|
|
<< " " << UI_sound_channel_last + 1 << ".." << n_of_channels - 1 << " - sound effects\n";
|
|
|
|
play_music();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void close_sound() {
|
|
int frequency, channels;
|
|
Uint16 format;
|
|
|
|
if(mix_ok) {
|
|
stop_bell();
|
|
stop_UI_sound();
|
|
stop_sound();
|
|
sound_cache.clear();
|
|
stop_music();
|
|
mix_ok = false;
|
|
|
|
int numtimesopened = Mix_QuerySpec(&frequency, &format, &channels);
|
|
if(numtimesopened == 0) {
|
|
ERR_AUDIO << "Error closing audio device: " << Mix_GetError() << std::endl;
|
|
}
|
|
while (numtimesopened) {
|
|
Mix_CloseAudio();
|
|
--numtimesopened;
|
|
}
|
|
}
|
|
if(SDL_WasInit(SDL_INIT_AUDIO) != 0)
|
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
|
|
LOG_AUDIO << "Audio device released.\n";
|
|
}
|
|
|
|
void reset_sound() {
|
|
bool music = preferences::music_on();
|
|
bool sound = preferences::sound_on();
|
|
bool UI_sound = preferences::UI_sound_on();
|
|
bool bell = preferences::turn_bell();
|
|
|
|
if (music || sound || bell || UI_sound) {
|
|
sound::close_sound();
|
|
if (!sound::init_sound()) {
|
|
ERR_AUDIO << "Error initializing audio device: " << Mix_GetError() << std::endl;
|
|
}
|
|
if (!music)
|
|
sound::stop_music();
|
|
if (!sound)
|
|
sound::stop_sound();
|
|
if (!UI_sound)
|
|
sound::stop_UI_sound();
|
|
if (!bell)
|
|
sound::stop_bell();
|
|
}
|
|
}
|
|
|
|
void stop_music() {
|
|
if(mix_ok) {
|
|
Mix_HaltMusic();
|
|
|
|
std::map<std::string,Mix_Music*>::iterator i;
|
|
for(i = music_cache.begin(); i != music_cache.end(); ++i)
|
|
Mix_FreeMusic(i->second);
|
|
music_cache.clear();
|
|
}
|
|
}
|
|
|
|
void stop_sound() {
|
|
if(mix_ok) {
|
|
Mix_HaltGroup(SOUND_SOURCES);
|
|
Mix_HaltGroup(SOUND_FX);
|
|
sound_cache_iterator itor = sound_cache.begin();
|
|
while(itor != sound_cache.end())
|
|
{
|
|
if(itor->group == SOUND_SOURCES || itor->group == SOUND_FX) {
|
|
itor = sound_cache.erase(itor);
|
|
} else {
|
|
++itor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* For the purpose of channel manipulation, we treat turn timer the same as bell
|
|
*/
|
|
void stop_bell() {
|
|
if(mix_ok) {
|
|
Mix_HaltGroup(SOUND_BELL);
|
|
Mix_HaltGroup(SOUND_TIMER);
|
|
sound_cache_iterator itor = sound_cache.begin();
|
|
while(itor != sound_cache.end())
|
|
{
|
|
if(itor->group == SOUND_BELL || itor->group == SOUND_TIMER) {
|
|
itor = sound_cache.erase(itor);
|
|
} else {
|
|
++itor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void stop_UI_sound() {
|
|
if(mix_ok) {
|
|
Mix_HaltGroup(SOUND_UI);
|
|
sound_cache_iterator itor = sound_cache.begin();
|
|
while(itor != sound_cache.end())
|
|
{
|
|
if(itor->group == SOUND_UI) {
|
|
itor = sound_cache.erase(itor);
|
|
} else {
|
|
++itor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void play_music_once(const std::string &file)
|
|
{
|
|
// Clear list so it's not replayed.
|
|
current_track_list.clear();
|
|
current_track = music_track(file);
|
|
play_music();
|
|
}
|
|
|
|
void empty_playlist()
|
|
{
|
|
current_track_list.clear();
|
|
}
|
|
|
|
void play_music()
|
|
{
|
|
music_start_time = 1; //immediate (same as effect as SDL_GetTicks())
|
|
want_new_music=true;
|
|
no_fading=false;
|
|
fadingout_time=current_track.ms_after();
|
|
}
|
|
|
|
static void play_new_music()
|
|
{
|
|
music_start_time = 0; //reset status: no start time
|
|
want_new_music = true;
|
|
|
|
if(!preferences::music_on() || !mix_ok || !current_track.valid()) {
|
|
return;
|
|
}
|
|
|
|
const std::string& filename = current_track.file_path();
|
|
|
|
std::map<std::string,Mix_Music*>::const_iterator itor = music_cache.find(filename);
|
|
if(itor == music_cache.end()) {
|
|
LOG_AUDIO << "attempting to insert track '" << filename << "' into cache\n";
|
|
|
|
#ifndef SDL_MIXER_OLD_VERSION
|
|
SDL_RWops *rwops = filesystem::load_RWops(filename);
|
|
Mix_Music* const music = Mix_LoadMUSType_RW(rwops, MUS_NONE, true); // SDL takes ownership of rwops
|
|
#else
|
|
Mix_Music* const music = Mix_LoadMUS(filename.c_str());
|
|
#endif
|
|
|
|
if(music == NULL) {
|
|
ERR_AUDIO << "Could not load music file '" << filename << "': "
|
|
<< Mix_GetError() << "\n";
|
|
return;
|
|
}
|
|
itor = music_cache.insert(std::pair<std::string,Mix_Music*>(filename,music)).first;
|
|
last_track=current_track;
|
|
}
|
|
|
|
LOG_AUDIO << "Playing track '" << filename << "'\n";
|
|
int fading_time=current_track.ms_before();
|
|
if(no_fading)
|
|
{
|
|
fading_time=0;
|
|
}
|
|
|
|
const int res = Mix_FadeInMusic(itor->second, 1, fading_time);
|
|
if(res < 0)
|
|
{
|
|
ERR_AUDIO << "Could not play music: " << Mix_GetError() << " " << filename <<" " << std::endl;
|
|
}
|
|
|
|
want_new_music=false;
|
|
}
|
|
|
|
void play_music_repeatedly(const std::string &id)
|
|
{
|
|
// Can happen if scenario doesn't specify.
|
|
if (id.empty())
|
|
return;
|
|
|
|
current_track_list.clear();
|
|
current_track_list.push_back(music_track(id));
|
|
|
|
// If we're already playing it, don't interrupt.
|
|
if (current_track != id) {
|
|
current_track = music_track(id);
|
|
play_music();
|
|
}
|
|
}
|
|
|
|
void play_music_config(const config &music_node)
|
|
{
|
|
music_track track( music_node );
|
|
|
|
if (!track.valid() && !track.id().empty()) {
|
|
ERR_AUDIO << "cannot open track '" << track.id() << "'; disabled in this playlist." << std::endl;
|
|
}
|
|
|
|
// If they say play once, we don't alter playlist.
|
|
if (track.play_once()) {
|
|
current_track = track;
|
|
play_music();
|
|
return;
|
|
}
|
|
|
|
// Clear play list unless they specify append.
|
|
if (!track.append()) {
|
|
current_track_list.clear();
|
|
}
|
|
|
|
if(track.valid()) {
|
|
// Avoid 2 tracks with the same name, since that can cause an infinite loop
|
|
// in choose_track(), 2 tracks with the same name will always return the
|
|
// current track and track_ok() doesn't allow that.
|
|
std::vector<music_track>::const_iterator itor = current_track_list.begin();
|
|
while(itor != current_track_list.end()) {
|
|
if(track == *itor) break;
|
|
++itor;
|
|
}
|
|
|
|
if(itor == current_track_list.end()) {
|
|
current_track_list.push_back(track);
|
|
} else {
|
|
ERR_AUDIO << "tried to add duplicate track '" << track.file_path() << "'" << std::endl;
|
|
}
|
|
}
|
|
|
|
// They can tell us to start playing this list immediately.
|
|
if (track.immediate()) {
|
|
current_track = track;
|
|
play_music();
|
|
}
|
|
}
|
|
|
|
void music_thinker::process(events::pump_info &info) {
|
|
if(preferences::music_on()) {
|
|
if(!music_start_time && !current_track_list.empty() && !Mix_PlayingMusic()) {
|
|
// Pick next track, add ending time to its start time.
|
|
current_track = choose_track();
|
|
music_start_time = info.ticks();
|
|
no_fading=true;
|
|
fadingout_time=0;
|
|
}
|
|
|
|
if(music_start_time && info.ticks(&music_refresh, music_refresh_rate) >= music_start_time - fadingout_time) {
|
|
want_new_music=true;
|
|
}
|
|
|
|
if(want_new_music) {
|
|
if(Mix_PlayingMusic()) {
|
|
Mix_FadeOutMusic(fadingout_time);
|
|
}
|
|
play_new_music();
|
|
}
|
|
}
|
|
}
|
|
|
|
void commit_music_changes()
|
|
{
|
|
played_before.clear();
|
|
|
|
// Play-once is OK if still playing.
|
|
if (current_track.play_once())
|
|
return;
|
|
|
|
// If current track no longer on playlist, change it.
|
|
BOOST_FOREACH(const music_track &m, current_track_list) {
|
|
if (current_track == m)
|
|
return;
|
|
}
|
|
|
|
// Victory empties playlist: if next scenario doesn't specify one...
|
|
if (current_track_list.empty())
|
|
return;
|
|
|
|
// FIXME: we don't pause ms_before on this first track. Should we?
|
|
current_track = choose_track();
|
|
play_music();
|
|
}
|
|
|
|
void write_music_play_list(config& snapshot)
|
|
{
|
|
// First entry clears playlist, others append to it.
|
|
bool append = false;
|
|
BOOST_FOREACH(music_track &m, current_track_list) {
|
|
m.write(snapshot, append);
|
|
append = true;
|
|
}
|
|
}
|
|
|
|
void reposition_sound(int id, unsigned int distance)
|
|
{
|
|
audio_lock lock;
|
|
for (unsigned ch = 0; ch < channel_ids.size(); ++ch)
|
|
{
|
|
if (channel_ids[ch] != id) continue;
|
|
if (distance >= DISTANCE_SILENT) {
|
|
// Don't call Mix_FadeOutChannel if the channel's volume is set to
|
|
// zero. It doesn't do anything in that case and the channel will
|
|
// resume playing as soon as its volume is reset to a non-zero
|
|
// value, which results in issues like sound sources deleted while
|
|
// their volume is zero coming back to life and escaping Wesnoth's
|
|
// sound source management code.
|
|
if (Mix_Volume(ch, -1) == 0) {
|
|
Mix_HaltChannel(ch);
|
|
} else {
|
|
Mix_FadeOutChannel(ch, 100);
|
|
}
|
|
} else {
|
|
Mix_SetDistance(ch, distance);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool is_sound_playing(int id)
|
|
{
|
|
audio_lock lock;
|
|
return std::find(channel_ids.begin(), channel_ids.end(), id) != channel_ids.end();
|
|
}
|
|
|
|
void stop_sound(int id)
|
|
{
|
|
reposition_sound(id, DISTANCE_SILENT);
|
|
}
|
|
|
|
void play_sound_positioned(const std::string &files, int id, int repeats, unsigned int distance)
|
|
{
|
|
if(preferences::sound_on()) {
|
|
play_sound_internal(files, SOUND_SOURCES, repeats, distance, id);
|
|
}
|
|
}
|
|
|
|
struct chunk_load_exception { };
|
|
|
|
static Mix_Chunk* load_chunk(const std::string& file, channel_group group)
|
|
{
|
|
sound_cache_iterator it_bgn, it_end;
|
|
sound_cache_iterator it;
|
|
|
|
sound_cache_chunk temp_chunk(file); // search the sound cache on this key
|
|
it_bgn = sound_cache.begin(), it_end = sound_cache.end();
|
|
it = std::find(it_bgn, it_end, temp_chunk);
|
|
|
|
if (it != it_end) {
|
|
if(it->group != group) {
|
|
// cached item has been used in multiple sound groups
|
|
it->group = NULL_CHANNEL;
|
|
}
|
|
|
|
//splice the most recently used chunk to the front of the cache
|
|
sound_cache.splice(it_bgn, sound_cache, it);
|
|
} else {
|
|
// remove the least recently used chunk from cache if it's full
|
|
bool cache_full = (sound_cache.size() == max_cached_chunks);
|
|
while( cache_full && it != it_bgn ) {
|
|
// make sure this chunk is not being played before freeing it
|
|
std::vector<Mix_Chunk*>::iterator ch_end = channel_chunks.end();
|
|
if(std::find(channel_chunks.begin(), ch_end, (--it)->get_data()) == ch_end) {
|
|
sound_cache.erase(it);
|
|
cache_full = false;
|
|
}
|
|
}
|
|
if(cache_full) {
|
|
LOG_AUDIO << "Maximum sound cache size reached and all are busy, skipping.\n";
|
|
throw chunk_load_exception();
|
|
}
|
|
temp_chunk.group = group;
|
|
std::string const &filename = filesystem::get_binary_file_location("sounds", file);
|
|
|
|
if (!filename.empty()) {
|
|
#ifndef SDL_MIXER_OLD_VERSION
|
|
SDL_RWops *rwops = filesystem::load_RWops(filename);
|
|
temp_chunk.set_data(Mix_LoadWAV_RW(rwops, true)); // SDL takes ownership of rwops
|
|
#else
|
|
temp_chunk.set_data(Mix_LoadWAV(filename.c_str()));
|
|
#endif
|
|
} else {
|
|
ERR_AUDIO << "Could not load sound file '" << file << "'." << std::endl;
|
|
throw chunk_load_exception();
|
|
}
|
|
|
|
if (temp_chunk.get_data() == NULL) {
|
|
ERR_AUDIO << "Could not load sound file '" << filename << "': "
|
|
<< Mix_GetError() << "\n";
|
|
throw chunk_load_exception();
|
|
}
|
|
|
|
sound_cache.push_front(temp_chunk);
|
|
}
|
|
|
|
return sound_cache.begin()->get_data();
|
|
}
|
|
|
|
void play_sound_internal(const std::string& files, channel_group group, unsigned int repeats,
|
|
unsigned int distance, int id, int loop_ticks, int fadein_ticks)
|
|
{
|
|
if(files.empty() || distance >= DISTANCE_SILENT || !mix_ok) {
|
|
return;
|
|
}
|
|
|
|
audio_lock lock;
|
|
|
|
// find a free channel in the desired group
|
|
int channel = Mix_GroupAvailable(group);
|
|
if(channel == -1) {
|
|
LOG_AUDIO << "All channels dedicated to sound group(" << group << ") are busy, skipping.\n";
|
|
return;
|
|
}
|
|
|
|
Mix_Chunk *chunk;
|
|
std::string file = pick_one(files);
|
|
|
|
try {
|
|
chunk = load_chunk(file, group);
|
|
assert(chunk);
|
|
} catch(const chunk_load_exception&) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* This check prevents SDL_Mixer from blowing up on Windows when UI sound is played
|
|
* in response to toggling the checkbox which disables sound.
|
|
*/
|
|
if(group != SOUND_UI) {
|
|
Mix_SetDistance(channel, distance);
|
|
}
|
|
|
|
int res;
|
|
if(loop_ticks > 0) {
|
|
if(fadein_ticks > 0) {
|
|
res = Mix_FadeInChannelTimed(channel, chunk, -1, fadein_ticks, loop_ticks);
|
|
} else {
|
|
res = Mix_PlayChannel(channel, chunk, -1);
|
|
}
|
|
|
|
if(res >= 0) {
|
|
Mix_ExpireChannel(channel, loop_ticks);
|
|
}
|
|
} else {
|
|
if(fadein_ticks > 0) {
|
|
res = Mix_FadeInChannel(channel, chunk, repeats, fadein_ticks);
|
|
} else {
|
|
res = Mix_PlayChannel(channel, chunk, repeats);
|
|
}
|
|
}
|
|
|
|
if(res < 0) {
|
|
ERR_AUDIO << "error playing sound effect: " << Mix_GetError() << std::endl;
|
|
//still keep it in the sound cache, in case we want to try again later
|
|
return;
|
|
}
|
|
|
|
channel_ids[channel] = id;
|
|
|
|
//reserve the channel's chunk from being freed, since it is playing
|
|
channel_chunks[res] = chunk;
|
|
}
|
|
|
|
void play_sound(const std::string& files, channel_group group, unsigned int repeats)
|
|
{
|
|
if(preferences::sound_on()) {
|
|
play_sound_internal(files, group, repeats);
|
|
}
|
|
}
|
|
|
|
// Play bell with separate volume setting
|
|
void play_bell(const std::string& files)
|
|
{
|
|
if (preferences::turn_bell()) {
|
|
play_sound_internal(files, SOUND_BELL);
|
|
}
|
|
}
|
|
|
|
// Play timer with separate volume setting
|
|
void play_timer(const std::string& files, int loop_ticks, int fadein_ticks)
|
|
{
|
|
if(preferences::sound_on()) {
|
|
play_sound_internal(files, SOUND_TIMER, 0, 0, -1, loop_ticks, fadein_ticks);
|
|
}
|
|
}
|
|
|
|
// Play UI sounds on separate volume than soundfx
|
|
void play_UI_sound(const std::string& files)
|
|
{
|
|
if(preferences::UI_sound_on()) {
|
|
play_sound_internal(files, SOUND_UI);
|
|
}
|
|
}
|
|
|
|
void set_music_volume(int vol)
|
|
{
|
|
if(mix_ok && vol >= 0) {
|
|
if(vol > MIX_MAX_VOLUME)
|
|
vol = MIX_MAX_VOLUME;
|
|
|
|
Mix_VolumeMusic(vol);
|
|
}
|
|
}
|
|
|
|
void set_sound_volume(int vol)
|
|
{
|
|
if(mix_ok && vol >= 0) {
|
|
if(vol > MIX_MAX_VOLUME)
|
|
vol = MIX_MAX_VOLUME;
|
|
|
|
// Bell, timer and UI have separate channels which we can't set up from this
|
|
for (unsigned i = 0; i < n_of_channels; ++i){
|
|
if(!(i >= UI_sound_channel_start && i <= UI_sound_channel_last)
|
|
&& i != bell_channel && i != timer_channel)
|
|
{
|
|
Mix_Volume(i, vol);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* For the purpose of volume setting, we treat turn timer the same as bell
|
|
*/
|
|
void set_bell_volume(int vol)
|
|
{
|
|
if(mix_ok && vol >= 0) {
|
|
if(vol > MIX_MAX_VOLUME)
|
|
vol = MIX_MAX_VOLUME;
|
|
|
|
Mix_Volume(bell_channel, vol);
|
|
Mix_Volume(timer_channel, vol);
|
|
}
|
|
}
|
|
|
|
void set_UI_volume(int vol)
|
|
{
|
|
if(mix_ok && vol >= 0) {
|
|
if(vol > MIX_MAX_VOLUME)
|
|
vol = MIX_MAX_VOLUME;
|
|
|
|
for (unsigned i = UI_sound_channel_start; i <= UI_sound_channel_last; ++i) {
|
|
Mix_Volume(i, vol);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string describe_versions()
|
|
{
|
|
std::stringstream ss;
|
|
|
|
#ifdef SDL_MIXER_VERSION
|
|
SDL_version compile_version;
|
|
SDL_MIXER_VERSION(&compile_version);
|
|
|
|
ss << "Compiled with SDL_mixer version: "
|
|
<< static_cast<int> (compile_version.major) << "."
|
|
<< static_cast<int> (compile_version.minor) << "."
|
|
<< static_cast<int> (compile_version.patch) << " \n";
|
|
#endif
|
|
|
|
#ifdef Mix_Linked_Version
|
|
const SDL_version *link_version=Mix_Linked_Version();
|
|
ss << "Running with SDL_mixer version: "
|
|
<< static_cast<int> (link_version->major) << "."
|
|
<< static_cast<int> (link_version->minor) << "."
|
|
<< static_cast<int> (link_version->patch) << " .\n";
|
|
#endif
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
} // end of sound namespace
|