Logo Search packages:      
Sourcecode: wesnoth-1.7 version File versions  Download package

sound.cpp

/* $Id: sound.cpp 40556 2010-01-03 18:45:02Z mordante $ */
/*
   Copyright (C) 2003 - 2010 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 version 2
   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 "filesystem.hpp"
#include "foreach.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 <list>

static lg::log_domain log_audio("audio");
#define LOG_AUDIO LOG_STREAM(info, log_audio)
#define ERR_AUDIO LOG_STREAM(err, log_audio)

namespace sound {
// Channel-chunk mapping lets us know, if we can safely free a given chunk
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)
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 = 16;

// 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 = n_of_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_channel = source_channel_last + 1;
const size_t n_reserved_channels = UI_sound_channel + 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;

}

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 "
                          << lexical_cast<std::string>(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());

      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() ));
      }

      //LOG_AUDIO << "Next track will be " << current_track_list[track].file_path() << "\n";
      played_before.push_back( current_track_list[track].file_path() );
      return current_track_list[track];
}

static std::string pick_one(const std::string &files)
{
      std::vector<std::string> ids = utils::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() << "\n";
                  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_GroupChannel(UI_sound_channel, 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";

            play_music();
      }
      return true;
}

void close_sound() {
      int numtimesopened, frequency, channels;
      Uint16 format;

      if(mix_ok) {
            stop_bell();
            stop_UI_sound();
            stop_sound();
            sound_cache.clear();
            stop_music();
            mix_ok = false;

            numtimesopened = Mix_QuerySpec(&frequency, &format, &channels);
            if(numtimesopened == 0) {
                  ERR_AUDIO << "Error closing audio device: " << Mix_GetError() << "\n";
            }
            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() << "\n";
            }
            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 = std::vector<music_track>();
      current_track = music_track(file);
      play_music();
}

void empty_playlist()
{
    current_track_list = std::vector<music_track>();
}

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";
            Mix_Music* const music = Mix_LoadMUS(filename.c_str());
            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 <<" \n";
      }

      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 = std::vector<music_track>(1, 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 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 = std::vector<music_track>();
      }

      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() << "'\n";
            }
      }
      else if(track.id().empty() == false) {
            ERR_AUDIO << "cannot open track '" << track.id() << "'; disabled in this playlist.\n";
      }

      // 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()
{
      std::vector<music_track>::iterator i;

      // Clear list of tracks played before.
      played_before = std::vector<std::string>();

      // Play-once is OK if still playing.
      if (current_track.play_once())
            return;

      // If current track no longer on playlist, change it.
      for (i = current_track_list.begin(); i != current_track_list.end(); ++i) {
            if (current_track == *i)
                  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)
{
      std::vector<music_track>::iterator i;
      bool append = false;

      // First entry clears playlist, others append to it.
      for (i = current_track_list.begin(); i != current_track_list.end(); ++i) {
            i->write(snapshot, append);
            append = true;
      }
}

void reposition_sound(int id, unsigned int distance)
{
      audio_lock lock;
      for(unsigned int ch = 0; ch < channel_ids.size(); ++ch) {
            int& ch_id = channel_ids[ch];
            if(ch_id == id) {
                  if(distance >= DISTANCE_SILENT) {
                        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 = get_binary_file_location("sounds", file);

            if (!filename.empty()) {
                  temp_chunk.set_data(Mix_LoadWAV(filename.c_str()));
            } else {
                  ERR_AUDIO << "Could not load sound file '" << file << "'.\n";
                  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() << "\n";
            //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 && 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;

            Mix_Volume(UI_sound_channel, vol);
      }
}

} // end of sound namespace

Generated by  Doxygen 1.6.0   Back to index