From a89589033baab1b3caee6be1f5c95f522ee778a1 Mon Sep 17 00:00:00 2001 From: Josh Yelon Date: Tue, 28 Aug 2007 16:28:41 +0000 Subject: [PATCH] More improvements and bugfixes --- panda/src/audiotraits/fmodAudioManager.cxx | 2 +- panda/src/audiotraits/fmodAudioSound.cxx | 53 +- panda/src/audiotraits/fmodAudioSound.h | 13 +- panda/src/audiotraits/openalAudioManager.cxx | 317 +++++------ panda/src/audiotraits/openalAudioManager.h | 52 +- panda/src/audiotraits/openalAudioSound.I | 61 +- panda/src/audiotraits/openalAudioSound.cxx | 561 +++++++++++++------ panda/src/audiotraits/openalAudioSound.h | 70 ++- 8 files changed, 726 insertions(+), 403 deletions(-) diff --git a/panda/src/audiotraits/fmodAudioManager.cxx b/panda/src/audiotraits/fmodAudioManager.cxx index 35d36e3244..1764e7fbae 100644 --- a/panda/src/audiotraits/fmodAudioManager.cxx +++ b/panda/src/audiotraits/fmodAudioManager.cxx @@ -341,7 +341,7 @@ make_dsp(const FilterProperties::FilterConfig &conf) { void FmodAudioManager:: update_dsp_chain(FMOD::DSP *head, FilterProperties *config) { const FilterProperties::ConfigVector &conf = config->get_config(); - FMOD_RESULT res1,res2,res3,res4,res5,res6; + FMOD_RESULT res1,res2,res3,res4,res5; while (1) { int numinputs; res1 = head->getNumInputs(&numinputs); diff --git a/panda/src/audiotraits/fmodAudioSound.cxx b/panda/src/audiotraits/fmodAudioSound.cxx index dc42324fc9..d2ccefabc7 100644 --- a/panda/src/audiotraits/fmodAudioSound.cxx +++ b/panda/src/audiotraits/fmodAudioSound.cxx @@ -282,14 +282,19 @@ get_loop_count() const { //////////////////////////////////////////////////////////////////// // Function: FmodAudioSound::set_time // Access: public -// Description: Sets the play position within the sound +// Description: Starts playing from the specified location. //////////////////////////////////////////////////////////////////// void FmodAudioSound:: set_time(float start_time) { FMOD_RESULT result; + if (!_active) { + _paused = true; + return; + } + int startTime = (int)(start_time * 1000); - + if (_channel != 0) { // try backing up current sound. result = _channel->setPosition( startTime , FMOD_TIMEUNIT_MS ); @@ -324,10 +329,8 @@ set_time(float start_time) { // add_dsp_on_channel(); set_3d_attributes_on_channel(); - if (_active) { - result = _channel->setPaused(false); - fmod_audio_errcheck("_channel->setPaused()", result); - } + result = _channel->setPaused(false); + fmod_audio_errcheck("_channel->setPaused()", result); _self_ref = this; } @@ -785,13 +788,26 @@ status() const { //////////////////////////////////////////////////////////////////// void FmodAudioSound:: set_active(bool active) { - _active = active; + if (_active != active) { + _active = active; + if (_active) { + // ...activate the sound. + if (_paused && get_loop_count()==0) { + // ...this sound was looping when it was paused. + _paused = false; + play(); + } - if (status() == PLAYING) { - // If the sound is (or should be) playing, then pause or unpause - // it in the system. - FMOD_RESULT result = _channel->setPaused(!_active); - fmod_audio_errcheck("_channel->setPaused()", result); + } else { + // ...deactivate the sound. + if (status() == PLAYING) { + if (get_loop_count() == 0) { + // ...we're pausing a looping sound. + _paused = true; + } + stop(); + } + } } } @@ -809,14 +825,11 @@ get_active() const { //////////////////////////////////////////////////////////////////// // Function: FmodAudioSound::finished // Access: public -// Description: NOT USED ANYMORE!!! -// Called by finishedCallback function when a sound -// terminates (but doesn't loop). +// Description: Not implemented. //////////////////////////////////////////////////////////////////// void FmodAudioSound:: finished() { - audio_debug("FmodAudioSound::finished()"); - audio_debug("NOT USED ANYMORE in FMOD-EX version of PANDA."); + audio_error("finished: not implemented under FMOD-EX"); } //////////////////////////////////////////////////////////////////// @@ -829,8 +842,7 @@ finished() { //////////////////////////////////////////////////////////////////// void FmodAudioSound:: set_finished_event(const string& event) { - audio_debug("FmodAudioSound::set_finished_event(event="<audio_channels(); int samples = (int)(source->length() * source->audio_rate()); int bytes = samples * channels * 2; - if (bytes > 200000000) { + if (bytes > 200000) { return false; } return true; } //////////////////////////////////////////////////////////////////// -// Function: OpenALAudioManager::load_sound_data +// Function: OpenALAudioManager::get_sound_data // Access: Private -// Description: Reads a sound file and creates a SoundData. -// When you are done with the SoundData, you need -// to decrement the client count. -//////////////////////////////////////////////////////////////////// -OpenALAudioManager::SoundData *OpenALAudioManager:: -load_sound_data(MovieAudioCursor *source) { - - nassertr(can_use_audio(source), NULL); - nassertr(can_load_audio(source), NULL); - - make_current(); - Filename path = source->get_source()->get_filename(); - SoundData *sd = new SoundData(this, path); - - alGetError(); // clear errors - sd->_buffer = 0; - alGenBuffers(1, &sd->_buffer); - al_audio_errcheck("alGenBuffers"); - if (sd->_buffer == 0) { - audio_error("Could not create an OpenAL buffer object"); - return NULL; - } - - int channels = source->audio_channels(); - int samples = (int)(source->length() * source->audio_rate()); - - PN_int16 *data = new PN_int16[samples * channels]; - source->read_samples(samples, data); - alBufferData(sd->_buffer, - (channels>1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16, - data, samples * channels * 2, source->audio_rate()); - int err = alGetError(); - if (err != AL_NO_ERROR) { - audio_error("alBufferData: " << alGetString(err)); - alDeleteBuffers(1, &sd->_buffer); - return NULL; - } - sd->_rate = source->audio_rate(); - sd->_channels = source->audio_channels(); - sd->_length = source->length(); - _all_sound_data.insert(SoundDataSet::value_type(path, sd)); - return sd; -} - -//////////////////////////////////////////////////////////////////// -// Function: OpenALAudioManager::cached_sound_data -// Access: Private -// Description: Looks in the cache for the specified sound data, -// and returns it if present. +// Description: Obtains a SoundData for the specified sound. // // When you are done with the SoundData, you need // to decrement the client count. //////////////////////////////////////////////////////////////////// OpenALAudioManager::SoundData *OpenALAudioManager:: -cached_sound_data(const Filename &path) { - SoundDataSet::const_iterator si=_all_sound_data.find(path); - if (si != _all_sound_data.end()) { - SoundData *sd = (*si).second; - increment_client_count(sd); - return sd; +get_sound_data(MovieAudio *movie) { + const Filename &path = movie->get_filename(); + + // Search for an already-cached sample or an already-opened stream. + if (!path.empty()) { + + SampleCache::iterator lsmi=_sample_cache.find(path); + if (lsmi != _sample_cache.end()) { + SoundData *sd = (*lsmi).second; + increment_client_count(sd); + return sd; + } + + ExpirationQueue::iterator exqi; + for (exqi=_expiring_streams.begin(); exqi!=_expiring_streams.end(); exqi++) { + SoundData *sd = (SoundData*)(*exqi); + if (sd->_movie->get_filename() == path) { + increment_client_count(sd); + return sd; + } + } } - return NULL; + + PT(MovieAudioCursor) stream = movie->open(); + if (stream == 0) { + audio_error("Cannot open file: "<_client_count = 1; + sd->_manager = this; + sd->_movie = movie; + sd->_rate = stream->audio_rate(); + sd->_channels = stream->audio_channels(); + sd->_length = stream->length(); + + audio_debug("Creating: " << sd->_movie->get_filename().get_basename()); + + if (can_load_audio(stream)) { + audio_debug(path.get_basename() << ": loading as sample"); + make_current(); + alGetError(); // clear errors + sd->_sample = 0; + alGenBuffers(1, &sd->_sample); + al_audio_errcheck("alGenBuffers"); + if (sd->_sample == 0) { + audio_error("Could not create an OpenAL buffer object"); + delete sd; + return NULL; + } + int channels = stream->audio_channels(); + int samples = (int)(stream->length() * stream->audio_rate()); + PN_int16 *data = new PN_int16[samples * channels]; + stream->read_samples(samples, data); + alBufferData(sd->_sample, + (channels>1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16, + data, samples * channels * 2, stream->audio_rate()); + int err = alGetError(); + if (err != AL_NO_ERROR) { + audio_error("could not fill OpenAL buffer object with data"); + delete sd; + return NULL; + } + _sample_cache.insert(SampleCache::value_type(path, sd)); + } else { + audio_debug(path.get_basename() << ": loading as stream"); + sd->_stream = stream; + } + + return sd; } //////////////////////////////////////////////////////////////////// @@ -361,39 +368,20 @@ get_sound(const string &file_name, bool positional) { return NULL; } - PT(MovieAudioCursor) cursor; - SoundData *sd = cached_sound_data(path); - if (sd == 0) { - cursor = MovieAudio::get(path)->open(); - if (cursor == 0) { - audio_error("Cannot open file: "<get_active()) { - _all_audio_sounds.insert(oas); - PT(AudioSound) res = (AudioSound*)(OpenALAudioSound*)oas; - return res; - } - return NULL; + _all_sounds.insert(oas); + PT(AudioSound) res = (AudioSound*)(OpenALAudioSound*)oas; + return res; } //////////////////////////////////////////////////////////////////// // Function: OpenALAudioManager::uncache_sound // Access: Public -// Description: Deletes a sound from the expiration queue. +// Description: Deletes a sample from the expiration queues. // If the sound is actively in use, then the sound // cannot be deleted, and this function has no effect. //////////////////////////////////////////////////////////////////// @@ -405,15 +393,13 @@ uncache_sound(const string& file_name) { VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr(); vfs->resolve_filename(path, get_sound_path()) || vfs->resolve_filename(path, get_model_path()); - - SoundDataSet::iterator sdi=_all_sound_data.find(path); - if (sdi != _all_sound_data.end()) { - SoundData *sd = (*sdi).second; - ExpirationQueue::iterator exi = - find(_expiration_queue.begin(), _expiration_queue.end(), sd); - if (exi != _expiration_queue.end()) { - _expiration_queue.erase(exi); - _all_sound_data.erase(sdi); + + SampleCache::iterator sci = _sample_cache.find(path); + if (sci != _sample_cache.end()) { + SoundData *sd = (*sci).second; + if (sd->_client_count == 0) { + _expiring_samples.erase(sd->_expire); + _sample_cache.erase(sci); delete sd; } } @@ -457,9 +443,9 @@ get_cache_limit() const { //////////////////////////////////////////////////////////////////// void OpenALAudioManager:: release_sound(OpenALAudioSound* audioSound) { - AudioSoundSet::iterator ai = _all_audio_sounds.find(audioSound); - if (ai != _all_audio_sounds.end()) { - _all_audio_sounds.erase(ai); + AllSounds::iterator ai = _all_sounds.find(audioSound); + if (ai != _all_sounds.end()) { + _all_sounds.erase(ai); } } @@ -470,13 +456,12 @@ release_sound(OpenALAudioSound* audioSound) { // Sets listener gain //////////////////////////////////////////////////////////////////// void OpenALAudioManager::set_volume(float volume) { - audio_debug("OpenALAudioManager::set_volume(volume="<get_short_time(); SoundsPlaying::iterator i=_sounds_playing.begin(); for (; i!=_sounds_playing.end(); ++i) { - if ((**i).status()!=AudioSound::PLAYING) { + OpenALAudioSound *sound = (*i); + sound->pull_used_buffers(); + sound->push_fresh_buffers(); + sound->restart_stalled_audio(); + sound->cache_time(rtc); + if (sound->status()!=AudioSound::PLAYING) { sounds_finished.insert(*i); } } - + i=sounds_finished.begin(); for (; i!=sounds_finished.end(); ++i) { (**i).finished(); @@ -885,19 +869,16 @@ update() { //////////////////////////////////////////////////////////////////// void OpenALAudioManager:: cleanup() { - audio_debug("OpenALAudioManager::cleanup(), this = " << (void *)this - << ", _cleanup_required = " << _cleanup_required); if (!_cleanup_required) { return; } - AudioSoundSet sounds(_all_audio_sounds); - AudioSoundSet::iterator ai; + AllSounds sounds(_all_sounds); + AllSounds::iterator ai; for (ai = sounds.begin(); ai != sounds.end(); ++ai) { (*ai)->cleanup(); } - assert(_all_sound_data.size() == _expiration_queue.size()); clear_cache(); nassertv(_active_managers > 0); @@ -938,7 +919,6 @@ cleanup() { } } _cleanup_required = false; - audio_debug("OpenALAudioManager::cleanup() finished"); } //////////////////////////////////////////////////////////////////// @@ -947,12 +927,15 @@ cleanup() { // Description: //////////////////////////////////////////////////////////////////// OpenALAudioManager::SoundData:: -SoundData(OpenALAudioManager* manager, const Filename &path) : - _manager(manager), - _path(path), - _buffer(0), - _client_count(1), - _length(0.0) +SoundData() : + _manager(0), + _movie(0), + _sample(0), + _stream(NULL), + _length(0.0), + _rate(0), + _channels(0), + _client_count(0) { } @@ -963,12 +946,12 @@ SoundData(OpenALAudioManager* manager, const Filename &path) : //////////////////////////////////////////////////////////////////// OpenALAudioManager::SoundData:: ~SoundData() { - if (_buffer != 0) { + if (_sample != 0) { if (_manager->_is_valid) { _manager->make_current(); - alDeleteBuffers(1,&_buffer); + alDeleteBuffers(1,&_sample); } - _buffer = 0; + _sample = 0; } } @@ -981,13 +964,14 @@ OpenALAudioManager::SoundData:: //////////////////////////////////////////////////////////////////// void OpenALAudioManager:: increment_client_count(SoundData *sd) { - audio_debug("Incrementing client count: " << sd->_path); sd->_client_count += 1; + audio_debug("Incrementing: " << sd->_movie->get_filename().get_basename() << " " << sd->_client_count); if (sd->_client_count == 1) { - audio_debug("Removing from expiration queue: " << sd->_path); - ExpirationQueue::iterator p=find(_expiration_queue.begin(), _expiration_queue.end(), sd); - assert(p != _expiration_queue.end()); - _expiration_queue.erase(p); + if (sd->_sample) { + _expiring_samples.erase(sd->_expire); + } else { + _expiring_streams.erase(sd->_expire); + } } } @@ -1002,11 +986,18 @@ increment_client_count(SoundData *sd) { //////////////////////////////////////////////////////////////////// void OpenALAudioManager:: decrement_client_count(SoundData *sd) { - audio_debug("Decrementing client count: " << sd->_path); sd->_client_count -= 1; + audio_debug("Decrementing: " << sd->_movie->get_filename().get_basename() << " " << sd->_client_count); if (sd->_client_count == 0) { - audio_debug("Adding to expiration queue: " << sd->_path); - _expiration_queue.push_back(sd); + if (sd->_sample) { + _expiring_samples.push_back(sd); + sd->_expire = _expiring_samples.end(); + sd->_expire--; + } else { + _expiring_streams.push_back(sd); + sd->_expire = _expiring_streams.end(); + sd->_expire--; + } discard_excess_cache(_cache_limit); } } @@ -1018,13 +1009,25 @@ decrement_client_count(SoundData *sd) { // number of sounds remaining is under the limit. //////////////////////////////////////////////////////////////////// void OpenALAudioManager:: -discard_excess_cache(int limit) { - while (((int)_expiration_queue.size()) > limit) { - SoundData *sd = _expiration_queue.front(); - audio_debug("Deleting head of sound cache: " << sd->_path); +discard_excess_cache(int sample_limit) { + int stream_limit = 5; + + while (((int)_expiring_samples.size()) > sample_limit) { + SoundData *sd = (SoundData*)(_expiring_samples.front()); assert(sd->_client_count == 0); - _expiration_queue.pop_front(); - _all_sound_data.erase(sd->_path); + assert(sd->_expire == _expiring_samples.begin()); + _expiring_samples.pop_front(); + _sample_cache.erase(_sample_cache.find(sd->_movie->get_filename())); + audio_debug("Expiring: " << sd->_movie->get_filename().get_basename()); + delete sd; + } + + while (((int)_expiring_streams.size()) > stream_limit) { + SoundData *sd = (SoundData*)(_expiring_streams.front()); + assert(sd->_client_count == 0); + assert(sd->_expire == _expiring_streams.begin()); + _expiring_streams.pop_front(); + audio_debug("Expiring: " << sd->_movie->get_filename().get_basename()); delete sd; } } diff --git a/panda/src/audiotraits/openalAudioManager.h b/panda/src/audiotraits/openalAudioManager.h index 01ff13e6d2..4867e3dc3a 100644 --- a/panda/src/audiotraits/openalAudioManager.h +++ b/panda/src/audiotraits/openalAudioManager.h @@ -25,9 +25,9 @@ #ifdef HAVE_OPENAL //[ #include "audioManager.h" -#include "pset.h" +#include "plist.h" #include "pmap.h" -#include "pdeque.h" +#include "pset.h" #include "movieAudioCursor.h" //The Includes needed for OpenAL @@ -115,12 +115,11 @@ class EXPCL_OPENAL_AUDIO OpenALAudioManager : public AudioManager { private: void make_current() const; - + bool can_use_audio(MovieAudioCursor *source); bool can_load_audio(MovieAudioCursor *source); - SoundData *cached_sound_data(const Filename &file_name); - SoundData *load_sound_data(MovieAudioCursor *source); + SoundData *get_sound_data(MovieAudio *source); // Tell the manager that the sound dtor was called. void release_sound(OpenALAudioSound* audioSound); @@ -135,6 +134,16 @@ private: private: + // An expiration queue is a list of SoundData + // that are no longer being used. They are kept + // around for a little while, since it is common to + // stop using a sound for a brief moment and then + // quickly resume. + + typedef plist ExpirationQueue; + ExpirationQueue _expiring_samples; + ExpirationQueue _expiring_streams; + // An AudioSound that uses a SoundData is called a "client" // of the SoundData. The SoundData keeps track of how // many clients are using it. When the number of clients @@ -148,27 +157,28 @@ private: class SoundData { public: - SoundData(OpenALAudioManager* manager, const Filename &path); + SoundData(); ~SoundData(); - OpenALAudioManager* _manager; - Filename _path; - ALuint _buffer; - double _length; - int _rate; - int _channels; - int _client_count; + OpenALAudioManager* _manager; + PT(MovieAudio) _movie; + ALuint _sample; + PT(MovieAudioCursor) _stream; + double _length; + int _rate; + int _channels; + int _client_count; + ExpirationQueue::iterator _expire; }; - typedef pmap SoundDataSet; - SoundDataSet _all_sound_data; + - typedef pset AudioSoundSet; - AudioSoundSet _all_audio_sounds; + typedef phash_map SampleCache; + SampleCache _sample_cache; - typedef pdeque ExpirationQueue; - ExpirationQueue _expiration_queue; - - typedef pset SoundsPlaying; + typedef phash_set SoundsPlaying; SoundsPlaying _sounds_playing; + + typedef phash_set AllSounds; + AllSounds _all_sounds; // State: int _cache_limit; diff --git a/panda/src/audiotraits/openalAudioSound.I b/panda/src/audiotraits/openalAudioSound.I index 743aab32be..ab80d2239a 100644 --- a/panda/src/audiotraits/openalAudioSound.I +++ b/panda/src/audiotraits/openalAudioSound.I @@ -16,8 +16,65 @@ // //////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::set_calibrated_clock +// Access: public +// Description: Sets the sound's calibrated clock. +// +// OpenAL is not very accurate at reporting how much +// time has elapsed within a buffer. However, it does +// accurately report when it has finished playing a +// buffer. So we use a hybrid clock algorithm. +// When OpenAL is in the middle of a buffer, +// we use a real-time-clock to estimate how far the +// sound has gotten. Each time OpenAL reaches the end +// of a buffer (which it does every 1/4 second or so), +// we calibrate our real-time-clock by speeding it up +// or slowing it down. +//////////////////////////////////////////////////////////////////// +INLINE void OpenALAudioSound:: +set_calibrated_clock(double rtc, double t, double accel) { + _calibrated_clock_scale = _playing_rate * accel; + _calibrated_clock_base = rtc - (t / _calibrated_clock_scale); +} +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::get_calibrated_clock +// Access: public +// Description: Returns the value of the calibrated clock. +//////////////////////////////////////////////////////////////////// +INLINE double OpenALAudioSound:: +get_calibrated_clock(double rtc) const { + return (rtc - _calibrated_clock_base) * _calibrated_clock_scale; +} +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::require_sound_data +// Access: Private +// Description: Makes sure the sound data record is present, +// and if not, obtains it. +//////////////////////////////////////////////////////////////////// +void OpenALAudioSound:: +require_sound_data() { + if (_sd==0) { + _sd = _manager->get_sound_data(_movie); + if (_sd==0) { + audio_error("Could not open audio " << _movie->get_filename()); + cleanup(); + } + } +} - - +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::release_sound_data +// Access: Private +// Description: Checks if the sound data record is present and +// releasable, and if so, releases it. +//////////////////////////////////////////////////////////////////// +void OpenALAudioSound:: +release_sound_data() { + if ((_sd!=0) && (!_movie->get_filename().empty())) { + _manager->decrement_client_count(_sd); + _sd = 0; + } +} diff --git a/panda/src/audiotraits/openalAudioSound.cxx b/panda/src/audiotraits/openalAudioSound.cxx index 54ba5a9c95..b947ba0678 100644 --- a/panda/src/audiotraits/openalAudioSound.cxx +++ b/panda/src/audiotraits/openalAudioSound.cxx @@ -45,42 +45,48 @@ TypeHandle OpenALAudioSound::_type_handle; OpenALAudioSound:: OpenALAudioSound(OpenALAudioManager* manager, - const Filename &path, - PT(MovieAudioCursor) stream, - OpenALAudioManager::SoundData *sample, + MovieAudio *movie, bool positional) : - _sample(sample), - _stream(stream), + _movie(movie), + _sd(NULL), + _loops_completed(0), + _playing_rate(0.0), + _playing_loops(0), _source(0), _manager(manager), - _path(path), + _basename(movie->get_filename().get_basename()), _volume(1.0f), _balance(0), - _play_rate(1.0), _loop_count(1), + _length(0.0), + _start_time(0.0), + _play_rate(1.0), + _current_time(0.0), _active(true), _paused(false) { - //Inits 3D Attributes - _location[0] = 0; _location[1] = 0; _location[2] = 0; - + _velocity[0] = 0; _velocity[1] = 0; _velocity[2] = 0; - + _min_dist = 3.28f; _max_dist = 1000000000.0f; _drop_off_factor = 1.0f; _positional = positional; - if (_positional) { - if ((_sample && (_sample->_channels != 1)) || - (_stream && (_stream->audio_channels() != 1))) { - audio_warning("Stereo sounds won't be spacialized: "<_length; + if (positional) { + if (_sd->_channels != 1) { + audio_warning("stereo sound " << movie->get_filename() << " will not be spatialized"); } } + release_sound_data(); } @@ -108,13 +114,12 @@ cleanup() { if (_source) { stop(); } - if (_sample) { - _manager->decrement_client_count(_sample); - _sample = 0; + if (_sd) { + _manager->decrement_client_count(_sd); + _sd = 0; } _manager->release_sound(this); _manager = 0; - _active = false; } //////////////////////////////////////////////////////////////////// @@ -124,9 +129,72 @@ cleanup() { //////////////////////////////////////////////////////////////////// void OpenALAudioSound:: play() { - set_time(0.0); -} + if (_manager == 0) return; + float px,py,pz,vx,vy,vz; + + if (!_active) { + _paused = true; + return; + } + + stop(); + + require_sound_data(); + if (_manager == 0) return; + _manager->starting_sound(this); + + if (!_source) { + return; + } + + _manager->make_current(); + + alGetError(); // clear errors + + // nonpositional sources are made relative to the listener so they don't move + alSourcei(_source,AL_SOURCE_RELATIVE,_positional?AL_FALSE:AL_TRUE); + al_audio_errcheck("alSourcei(_source,AL_SOURCE_RELATIVE)"); + + // set source properties that we have stored + set_volume(_volume); + //set_balance(_balance); + + set_3d_min_distance(_min_dist); + set_3d_max_distance(_max_dist); + set_3d_drop_off_factor(_drop_off_factor); + get_3d_attributes(&px,&py,&pz,&vx,&vy,&vz); + set_3d_attributes(px, py, pz, vx, vy, vz); + + _playing_loops = _loop_count; + if (_playing_loops == 0) { + _playing_loops = 1000000000; + } + _loops_completed = 0; + + double play_rate = _play_rate * _manager->get_play_rate(); + audio_debug("playing. Rate=" << play_rate); + alSourcef(_source, AL_PITCH, play_rate); + _playing_rate = play_rate; + + if (_sd->_sample) { + push_fresh_buffers(); + alSourcef(_source, AL_SEC_OFFSET, _start_time); + _stream_queued[0]._time_offset = _start_time; + restart_stalled_audio(); + } else { + audio_debug("Play: stream tell = " << _sd->_stream->tell() << " seeking " << _start_time); + if (_sd->_stream->tell() != _start_time) { + _sd->_stream->seek(_start_time); + } + push_fresh_buffers(); + restart_stalled_audio(); + } + double rtc = TrueClock::get_global_ptr()->get_short_time(); + set_calibrated_clock(rtc, _start_time, 1.0); + _current_time = _start_time; + _start_time = 0.0; +} //////////////////////////////////////////////////////////////////// // Function: OpenALAudioSound::stop @@ -135,24 +203,28 @@ play() { //////////////////////////////////////////////////////////////////// void OpenALAudioSound:: stop() { - openal_audio_debug("stop()"); - //nassertv(_source); - + if (_manager==0) return; + if (_source) { _manager->make_current(); alGetError(); // clear errors alSourceStop(_source); - al_audio_errcheck("alSourceStop(_source)"); + al_audio_errcheck("stopping a source"); + alSourcei(_source, AL_BUFFER, 0); + al_audio_errcheck("clear source buffers"); + for (int i=0; i<((int)(_stream_queued.size())); i++) { + ALuint buffer = _stream_queued[i]._buffer; + if (buffer != _sd->_sample) { + alDeleteBuffers(1, &buffer); + al_audio_errcheck("deleting a buffer"); + } + } + _stream_queued.resize(0); } - + _manager->stopping_sound(this); - // The _paused flag should not be cleared here. _paused is not like - // the Pause button on a cd/dvd player. It is used as a flag to say - // that it was looping when it was set inactive. There is no need to - // make this symmetrical with play(). set_active() is the 'owner' of - // _paused. play() accesses _paused to help in the situation where - // someone calls play on an inactive sound(). + release_sound_data(); } //////////////////////////////////////////////////////////////////// @@ -162,8 +234,8 @@ stop() { //////////////////////////////////////////////////////////////////// void OpenALAudioSound:: finished() { - openal_audio_debug("finished()"); - _manager->stopping_sound(this); + stop(); + _current_time = _length; if (!_finished_event.empty()) { throw_event(_finished_event); } @@ -186,7 +258,7 @@ set_loop(bool loop) { //////////////////////////////////////////////////////////////////// bool OpenALAudioSound:: get_loop() const { - return (_loop_count != 1); + return (_loop_count == 0); } //////////////////////////////////////////////////////////////////// @@ -196,27 +268,12 @@ get_loop() const { //////////////////////////////////////////////////////////////////// void OpenALAudioSound:: set_loop_count(unsigned long loop_count) { - openal_audio_debug("set_loop_count(loop_count="<1) { - audio_error("OpenALAudioSound::set_loop_count() doesn't support looping a finite number of times, 0 (infinite) or 1 only"); - loop_count = 1; - } + if (_manager==0) return; - if (_loop_count==loop_count) { - return; + if (loop_count >= 1000000000) { + loop_count = 0; } - _loop_count=loop_count; - - if (_source) { - // I believe there is a race condition here. - _manager->make_current(); - - alGetError(); // clear errors - alSourcei(_source,AL_LOOPING,_loop_count==0?AL_TRUE:AL_FALSE); - al_audio_errcheck("alSourcei(_source,AL_LOOPING)"); - } } //////////////////////////////////////////////////////////////////// @@ -229,70 +286,250 @@ get_loop_count() const { return _loop_count; } +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::restart_stalled_audio +// Access: public +// Description: When streaming audio, the computer is supposed to +// keep OpenAL's queue full. However, there are times +// when the computer is running slow and the queue +// empties prematurely. In that case, OpenAL will stop. +// When the computer finally gets around to refilling +// the queue, it is necessary to tell OpenAL to resume +// playing. +//////////////////////////////////////////////////////////////////// +void OpenALAudioSound:: +restart_stalled_audio() { + ALenum status; + if (_stream_queued.size() == 0) { + return; + } + alGetError(); + alGetSourcei(_source, AL_SOURCE_STATE, &status); + if (status != AL_PLAYING) { + alSourcePlay(_source); + } +} + +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::queue_buffer +// Access: public +// Description: Pushes a buffer into the source queue. +//////////////////////////////////////////////////////////////////// +void OpenALAudioSound:: +queue_buffer(ALuint buffer, int loop_index, double time_offset) { + // Now push the buffer into the stream queue. + alGetError(); + alSourceQueueBuffers(_source,1,&buffer); + ALenum err = alGetError(); + if (err != AL_NO_ERROR) { + audio_error("could not load sample buffer into the queue"); + cleanup(); + return; + } + QueuedBuffer buf; + buf._buffer = buffer; + buf._loop_index = loop_index; + buf._time_offset = time_offset; + _stream_queued.push_back(buf); + // audio_debug("Buffer queued " << loop_index << " " << time_offset); +} + +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::make_buffer +// Access: public +// Description: Creates an OpenAL buffer object. +//////////////////////////////////////////////////////////////////// +ALuint OpenALAudioSound:: +make_buffer(int samples, int channels, int rate, unsigned char *data) { + + // Allocate a buffer to hold the data. + alGetError(); + ALuint buffer; + alGenBuffers(1, &buffer); + if (alGetError() != AL_NO_ERROR) { + audio_error("could not allocate an OpenAL buffer object"); + cleanup(); + return 0; + } + + // Now fill the buffer with the data provided. + alBufferData(buffer, + (channels>1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16, + data, samples * channels * 2, rate); + int err = alGetError(); + if (err != AL_NO_ERROR) { + audio_error("could not fill OpenAL buffer object with data"); + cleanup(); + return 0; + } + + return buffer; +} + +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::read_stream_data +// Access: public +// Description: Fills a buffer with data from the stream. +// Returns the number of samples stored in the buffer. +//////////////////////////////////////////////////////////////////// +int OpenALAudioSound:: +read_stream_data(int bytelen, unsigned char *buffer) { + + MovieAudioCursor *cursor = _sd->_stream; + double length = cursor->length(); + int channels = cursor->audio_channels(); + int rate = cursor->audio_rate(); + int space = bytelen / (channels * 2); + int fill = 0; + + while (space && (_loops_completed < _playing_loops)) { + double t = cursor->tell(); + double remain = length - t; + if (remain > 60.0) { + remain = 60.0; + } + int samples = (int)(remain * rate); + if (samples <= 0) { + _loops_completed += 1; + cursor->seek(0.0); + continue; + } + if (samples > space) { + samples = space; + } + cursor->read_samples(samples, (PN_int16 *)buffer); + size_t hval = AddHash::add_hash(0, (PN_uint8*)buffer, samples*channels*2); + audio_debug("Streaming " << cursor->get_source()->get_filename().get_basename() << " at " << t << " hash " << hval); + fill += samples; + space -= samples; + buffer += (samples * channels * 2); + } + return fill; +} + +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::correct_calibrated_clock +// Access: public +// Description: Compares the specified time to the value of the +// calibrated clock, and adjusts the calibrated +// clock speed to make it closer to the target value. +// This routine is quite careful to make sure that +// the calibrated clock moves in a smooth, monotonic +// way. +//////////////////////////////////////////////////////////////////// +void OpenALAudioSound:: +correct_calibrated_clock(double rtc, double t) { + double cc = (rtc - _calibrated_clock_base) * _calibrated_clock_scale; + double diff = cc-t; + _calibrated_clock_decavg = (_calibrated_clock_decavg * 0.95) + (diff * 0.05); + if (diff > 0.5) { + set_calibrated_clock(rtc, t, 1.0); + _calibrated_clock_decavg = 0.0; + } else { + double scale = 1.0; + if ((_calibrated_clock_decavg > 0.01) && (diff > 0.01)) { + scale = 0.98; + } + if ((_calibrated_clock_decavg < -0.01) && (diff < -0.01)) { + scale = 1.03; + } + if ((_calibrated_clock_decavg < -0.05) && (diff < -0.05)) { + scale = 1.2; + } + if ((_calibrated_clock_decavg < -0.15) && (diff < -0.15)) { + scale = 1.5; + } + set_calibrated_clock(rtc, cc, scale); + } + cc = (rtc - _calibrated_clock_base) * _calibrated_clock_scale; +} + +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::pull_used_buffers +// Access: public +// Description: Pulls any used buffers out of OpenAL's queue. +//////////////////////////////////////////////////////////////////// +void OpenALAudioSound:: +pull_used_buffers() { + while (_stream_queued.size()) { + ALuint buffer = 0; + alGetError(); + alSourceUnqueueBuffers(_source, 1, &buffer); + int err = alGetError(); + if (err == AL_NO_ERROR) { + if (_stream_queued[0]._buffer != buffer) { + audio_error("corruption in stream queue"); + cleanup(); + return; + } + _stream_queued.pop_front(); + if (_stream_queued.size()) { + double al = _stream_queued[0]._time_offset + _stream_queued[0]._loop_index * _length; + double rtc = TrueClock::get_global_ptr()->get_short_time(); + correct_calibrated_clock(rtc, al); + } + if (buffer != _sd->_sample) { + alDeleteBuffers(1,&buffer); + } + } else { + break; + } + } +} + +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::push_fresh_buffers +// Access: public +// Description: Pushes fresh buffers into OpenAL's queue until +// the queue is "full" (ie, has plenty of data). +//////////////////////////////////////////////////////////////////// +void OpenALAudioSound:: +push_fresh_buffers() { + static unsigned char data[65536]; + + if (_sd->_sample) { + while ((_loops_completed < _playing_loops) && + (_stream_queued.size() < 100)) { + queue_buffer(_sd->_sample, _loops_completed, 0.0); + _loops_completed += 1; + } + } else { + MovieAudioCursor *cursor = _sd->_stream; + int channels = cursor->audio_channels(); + int rate = cursor->audio_rate(); + double space = 65536 / (channels * 2); + + // Calculate how many buffers to keep in the queue. + int fill_to = (int)((audio_buffering_seconds * rate) / space) + 1; + if (fill_to < 3) { + fill_to = 3; + } + + while ((_loops_completed < _playing_loops) && + (((int)(_stream_queued.size())) < fill_to)) { + int loop_index = _loops_completed; + double time_offset = cursor->tell(); + int samples = read_stream_data(65536, data); + if (samples == 0) { + break; + } + ALuint buffer = make_buffer(samples, channels, rate, data); + if (_manager == 0) return; + queue_buffer(buffer, loop_index, time_offset); + if (_manager == 0) return; + } + } +} + //////////////////////////////////////////////////////////////////// // Function: OpenALAudioSound::set_time // Access: public -// Description: Sets the play position within the sound +// Description: The next time you call play, the sound will +// start from the specified offset. //////////////////////////////////////////////////////////////////// void OpenALAudioSound:: set_time(float time) { - float px,py,pz,vx,vy,vz; - - openal_audio_debug("play()"); - if (!_active) { - _paused=true; - return; - } - - if (status() == AudioSound::PLAYING) { - stop(); - } - - _manager->starting_sound(this); - - if (!_source) { - return; - } - - // Setup source - _manager->make_current(); - - alGetError(); // clear errors - - // Assign the buffer to the source - alSourcei(_source,AL_BUFFER,_sample->_buffer); - ALenum result = alGetError(); - if (result!=AL_NO_ERROR) { - audio_error("alSourcei(_source,AL_BUFFER,_sample->_buffer): " << alGetString(result) ); - stop(); - return; - } - - // nonpositional sources are made relative to the listener so they don't move - alSourcei(_source,AL_SOURCE_RELATIVE,_positional?AL_FALSE:AL_TRUE); - al_audio_errcheck("alSourcei(_source,AL_SOURCE_RELATIVE)"); - - // set source properties that we have stored - set_volume(_volume); - //set_balance(_balance); - set_play_rate(_play_rate); - set_3d_min_distance(_min_dist); - set_3d_max_distance(_max_dist); - set_3d_drop_off_factor(_drop_off_factor); - get_3d_attributes(&px,&py,&pz,&vx,&vy,&vz); - set_3d_attributes(px, py, pz, vx, vy, vz); - - set_loop_count(_loop_count); - - openal_audio_debug("set_time(time="<make_current(); - alGetError(); // clear errors - alGetSourcef(_source,AL_SEC_OFFSET,&time); - al_audio_errcheck("alGetSourcef(_source,AL_SEC_OFFSET)"); - } else { + if (_manager == 0) { return 0.0; } - - return time; + return _current_time; +} + +//////////////////////////////////////////////////////////////////// +// Function: OpenALAudioSound::cache_time +// Access: Private +// Description: Updates the current_time field of a playing sound. +//////////////////////////////////////////////////////////////////// +void OpenALAudioSound:: +cache_time(double rtc) { + assert(_source != 0); + double t=get_calibrated_clock(rtc); + double max = _length * _playing_loops; + if (t >= max) { + _current_time = _length; + } else { + _current_time = fmod(t, _length); + } } //////////////////////////////////////////////////////////////////// @@ -324,17 +570,11 @@ get_time() const { //////////////////////////////////////////////////////////////////// void OpenALAudioSound:: set_volume(float volume) { - openal_audio_debug("set_volume(volume="<get_volume(); - _manager->make_current(); - alGetError(); // clear errors alSourcef(_source,AL_GAIN,volume); al_audio_errcheck("alSourcef(_source,AL_GAIN)"); @@ -348,7 +588,6 @@ set_volume(float volume) { //////////////////////////////////////////////////////////////////// float OpenALAudioSound:: get_volume() const { - openal_audio_debug("get_volume() returning "<<_volume); return _volume; } @@ -384,21 +623,7 @@ get_balance() const { //////////////////////////////////////////////////////////////////// void OpenALAudioSound:: set_play_rate(float play_rate) { - openal_audio_debug("set_play_rate(play_rate="<get_play_rate(); - - _manager->make_current(); - - alGetError(); // clear errors - alSourcef(_source,AL_PITCH,play_rate); - al_audio_errcheck("alSourcef(_source,AL_PITCH)"); - } + _play_rate = play_rate; } //////////////////////////////////////////////////////////////////// @@ -408,7 +633,6 @@ set_play_rate(float play_rate) { //////////////////////////////////////////////////////////////////// float OpenALAudioSound:: get_play_rate() const { - openal_audio_debug("get_play_rate() returning "<<_play_rate); return _play_rate; } @@ -419,23 +643,24 @@ get_play_rate() const { //////////////////////////////////////////////////////////////////// float OpenALAudioSound:: length() const { - return _sample->_length; + return _length; } //////////////////////////////////////////////////////////////////// // Function: OpenALAudioSound::set_3d_attributes // Access: public // Description: Set position and velocity of this sound -// NOW LISTEN UP!!! THIS IS IMPORTANT! -// Both Panda3D and OpenAL use a right handed coordinate system. -// But there is a major difference! -// In Panda3D the Y-Axis is going into the Screen and the Z-Axis is going up. -// In OpenAL the Y-Axis is going up and the Z-Axis is coming out of the screen. -// The solution is simple, we just flip the Y and Z axis and negate the Z, as we move coordinates -// from Panda to OpenAL and back. -// What does did mean to average Panda user? Nothing, they shouldn't notice anyway. -// But if you decide to do any 3D audio work in here you have to keep it in mind. -// I told you, so you can't say I didn't. +// +// Both Panda3D and OpenAL use a right handed +// coordinate system. However, in Panda3D the +// Y-Axis is going into the Screen and the +// Z-Axis is going up. In OpenAL the Y-Axis is +// going up and the Z-Axis is coming out of +// the screen. +// +// The solution is simple, we just flip the Y +// and Z axis and negate the Z, as we move +// coordinates from Panda to OpenAL and back. //////////////////////////////////////////////////////////////////// void OpenALAudioSound:: set_3d_attributes(float px, float py, float pz, float vx, float vy, float vz) { @@ -572,14 +797,11 @@ get_3d_drop_off_factor() const { //////////////////////////////////////////////////////////////////// void OpenALAudioSound:: set_active(bool active) { - openal_audio_debug("set_active(active="<make_current(); - alGetError(); // clear errors - alGetSourcei(_source,AL_SOURCE_STATE,&status); - al_audio_errcheck("alGetSourcei(_source,AL_SOURCE_STATE)"); - - if (status == AL_PLAYING/* || status == AL_PAUSED*/) { - return AudioSound::PLAYING; - } else { + if (_stream_queued.size() == 0) { return AudioSound::READY; + } else { + return AudioSound::PLAYING; } } diff --git a/panda/src/audiotraits/openalAudioSound.h b/panda/src/audiotraits/openalAudioSound.h index 2aa42d1256..ce1c1ff1b0 100644 --- a/panda/src/audiotraits/openalAudioSound.h +++ b/panda/src/audiotraits/openalAudioSound.h @@ -28,6 +28,7 @@ #include "audioSound.h" #include "movieAudioCursor.h" +#include "trueClock.h" #include #include @@ -89,7 +90,7 @@ public: const string& get_finished_event() const; const string &get_name() const; - + // return: playing time in seconds. float length() const; @@ -114,18 +115,39 @@ public: private: OpenALAudioSound(OpenALAudioManager* manager, - const Filename &path, - PT(MovieAudioCursor) cursor, - OpenALAudioManager::SoundData *sd, + MovieAudio *movie, bool positional); + INLINE void set_calibrated_clock(double rtc, double t, double playrate); + INLINE double get_calibrated_clock(double rtc) const; + void correct_calibrated_clock(double rtc, double t); + void cache_time(double rtc); void cleanup(); - + void restart_stalled_audio(); + void delete_queued_buffers(); + ALuint make_buffer(int samples, int channels, int rate, unsigned char *data); + void queue_buffer(ALuint buffer, int loop_index, double time_offset); + int read_stream_data(int bytelen, unsigned char *data); + void pull_used_buffers(); + void push_fresh_buffers(); + INLINE void require_sound_data(); + INLINE void release_sound_data(); + private: + + PT(MovieAudio) _movie; + OpenALAudioManager::SoundData *_sd; - // A Sound can have a sample or a stream, but not both. - OpenALAudioManager::SoundData *_sample; - PT(MovieAudioCursor) _stream; - ALuint _stream_buffers[3]; + struct QueuedBuffer { + ALuint _buffer; + int _loop_index; + double _time_offset; + }; + + int _playing_loops; + float _playing_rate; + + pdeque _stream_queued; + int _loops_completed; ALuint _source; PT(OpenALAudioManager) _manager; @@ -133,7 +155,7 @@ private: float _volume; // 0..1.0 float _balance; // -1..1 float _play_rate; // 0..1.0 - + bool _positional; ALfloat _location[3]; ALfloat _velocity[3]; @@ -141,28 +163,40 @@ private: float _min_dist; float _max_dist; float _drop_off_factor; + + double _length; + int _loop_count; - mutable float _length; // in seconds. - unsigned long _loop_count; + // The calibrated clock is initialized when the + // sound starts playing, and is periodically corrected + // thereafter. + double _calibrated_clock_base; + double _calibrated_clock_scale; + double _calibrated_clock_decavg; + + // The start_time field affects the next call to play. + double _start_time; + // The current_time field is updated every frame + // during the AudioManager update. Updates need + // to be atomic, because get_time can be called + // in the cull thread. + float _current_time; + // This is the string that throw_event() will throw // when the sound finishes playing. It is not triggered // when the sound is stopped with stop(). string _finished_event; - Filename _path; + Filename _basename; // _active is for things like a 'turn off sound effects' in // a preferences pannel. // _active is not about whether a sound is currently playing. // Use status() for info on whether the sound is playing. bool _active; - - // _paused is not like the Pause button on a cd/dvd player. - // It is used as a flag to say that the sound was looping when - // itwas set inactive. bool _paused; - + public: static TypeHandle get_class_type() { return _type_handle;