/*] Copyright (c) 2009-2011, Charles McGarvey [***************************** **] All rights reserved. * * Distributable under the terms and conditions of the 2-clause BSD license; * see the file COPYING for a complete text of the license. * *****************************************************************************/ #include #include #include #include #include #include #include #include #include #include "debug.hh" #include "sound.hh" #include "resource.hh" #include "runloop.hh" #include "thread.hh" #include "timer.hh" #ifndef BUF_SIZE #define BUF_SIZE 8192 #endif #define NUM_SAMPLE_BITS 16 namespace moof { thread stream_thread_; class sound_backend { public: sound_backend() { if (retain_count++ == 0) { al_device = alcOpenDevice(0); al_context = alcCreateContext(al_device, 0); if (!al_device || !al_context) { const char* error = alcGetString(al_device, alcGetError(al_device)); log_error("audio subsystem initialization failure", error); } else { alcMakeContextCurrent(al_context); log_info << "opened sound device `" << alcGetString(al_device, ALC_DEFAULT_DEVICE_SPECIFIER) << "'" << std::endl; } } } sound_backend(const sound_backend& backend) { ++retain_count; } sound_backend& operator = (const sound_backend& backend) { ++retain_count; return *this; } ~sound_backend() { if (--retain_count == 0) { alcMakeContextCurrent(0); alcDestroyContext(al_context); alcCloseDevice(al_device); } } static int retain_count; static ALCdevice* al_device; static ALCcontext* al_context; }; int sound_backend::retain_count = 0; ALCdevice* sound_backend::al_device; ALCcontext* sound_backend::al_context; class sound_resource; typedef resource_handle sound_handle; MOOF_REGISTER_RESOURCE(sound_resource, ogg, sounds); class sound_resource : public boost::noncopyable { public: sound_resource(const std::string& path) : buffer_(AL_NONE), sample_(0) { if (ov_fopen((char*)path.c_str(), &file_) < 0) throw std::runtime_error("problem reading audio: " + path); } ~sound_resource() { ov_clear(&file_); if (buffer_) alDeleteBuffers(1, &buffer_); } ALuint read_all() const { if (buffer_) return buffer_; if (ov_pcm_seek(&file_, 0) != 0) { log_warning("vorbis seek error"); return AL_NONE; } ogg_int64_t samples = ov_pcm_total(&file_, 0); char data[2 * samples * (NUM_SAMPLE_BITS / 8)]; size_t size = 0; while (size < sizeof(data)) { int section; int result = ov_read(&file_, data + size, sizeof(data) - size, 0, 2, 1, §ion); if (0 < result) { size += result; } else if (result == 0 && size > 0) { vorbis_info* info = ov_info(&file_, section); ALuint buffer; alGenBuffers(1, &buffer); alBufferData(buffer, get_audio_format(info), data, size, info->rate); buffer_ = buffer; return buffer; } else { log_warning("vorbis playback error"); break; } } return AL_NONE; } bool read(ALuint buffer, uint64_t& sample) const { if ((sample == sample_ && ov_pcm_seek_lap(&file_, sample) != 0) || (sample != sample_ && ov_pcm_seek(&file_, sample) != 0)) return false; char data[BUF_SIZE]; int section; int result = ov_read(&file_, data, sizeof(data), 0, 2, 1, §ion); if (0 < result) { vorbis_info* info = ov_info(&file_, section); alBufferData(buffer, get_audio_format(info), data, result, info->rate); sample_ = sample = ov_pcm_tell(&file_); return true; } else if (result < 0) { log_warning("vorbis playback error"); } return false; } bool read(ALuint buffer, uint64_t& sample, sound_handle other) const { ov_crosslap(&other->file_, &file_); char data[BUF_SIZE]; int section; int result = ov_read(&file_, data, sizeof(data), 0, 2, 1, §ion); if (0 < result) { vorbis_info* info = ov_info(&file_, section); alBufferData(buffer, get_audio_format(info), data, result, info->rate); sample_ = sample = ov_pcm_tell(&file_); return true; } else if (result < 0) { log_warning("vorbis playback error"); } return false; } // determines the correct number of fixed-size buffers needed to hold // a given number of seconds of PCM audio. int num_buffers(scalar seconds) const { vorbis_info* info = ov_info(&file_, -1); int count = scalar(info->rate) * // sample rate scalar(info->channels) * // channels scalar(NUM_SAMPLE_BITS) * // sample size seconds * // time SCALAR(0.125) / // bits to bytes scalar(BUF_SIZE); // individual buffer size return count; } static ALenum get_audio_format(const vorbis_info* info) { if (info->channels == 1) return AL_FORMAT_MONO16; else if (info->channels == 2) return AL_FORMAT_STEREO16; log_error("unsupported number of channels:", info->channels); return 0; } private: mutable OggVorbis_File file_; mutable ALuint buffer_; mutable uint64_t sample_; sound_backend backend_; }; class sound::impl { public: impl() { init(); } impl(const std::string& name) { init(); sample(name); } void init() { is_playing_ = false; is_looping_ = false; sample_ = 0; buffer_size_ = SCALAR(1.0); alGenSources(1, &source_); ALfloat zero[] = {0.0f, 0.0f, 0.0f}; alSourcef(source_, AL_PITCH, 1.0f); alSourcef(source_, AL_GAIN, 1.0f); alSourcefv(source_, AL_POSITION, zero); alSourcefv(source_, AL_VELOCITY, zero); } ~impl() { stop(); deplete_stream(); alDeleteSources(1, &source_); } void deplete_stream() { if (is_streaming_) { ALint queued = 0; alGetSourcei(source_, AL_BUFFERS_QUEUED, &queued); ALuint buffers[queued]; alSourceUnqueueBuffers(source_, queued, buffers); alDeleteBuffers(queued, buffers); } else { alSourcei(source_, AL_BUFFER, AL_NONE); } } void play() { if (queue_.empty() || is_playing_) return; ALenum state; alGetSourcei(source_, AL_SOURCE_STATE, &state); switch (state) { case AL_INITIAL: case AL_STOPPED: if (is_streaming_) { start_update_timer(); num_buffers_ = queue_.front()->num_buffers(buffer_size_); fill_stream(); is_playing_ = true; alSourcei(source_, AL_LOOPING, false); } else { ALuint buffer = queue_.front()->read_all(); alSourcei(source_, AL_BUFFER, buffer); alSourcei(source_, AL_LOOPING, is_looping_); } break; case AL_PAUSED: if (is_streaming_) { start_update_timer(); is_playing_ = true; } break; } alSourcePlay(source_); } void start_update_timer() { stream_timer_.init(boost::bind(&impl::stream_update, this, _1, _2), SCALAR(0.1), timer::repeat); thread::main_runloop().add_timer(stream_timer_); //thread thread; //thread.runloop().add_timer(stream_timer_); //if (!stream_thread_.is_valid()) //stream_thread_ = thread::detach(stream_timer_); // FIXME: proper locking needs to be done; this is causing crashes // on shutdown when sound resources are destroyed yet the stream // thread is neither shutdown or notified. } void fill_stream() { ALenum type; alGetSourcei(source_, AL_SOURCE_TYPE, &type); ASSERT(type != AL_STATIC && "source shouldn't be static"); ALint processed = 0; ALint queued = 0; alGetSourcei(source_, AL_BUFFERS_QUEUED, &queued); alGetSourcei(source_, AL_BUFFERS_PROCESSED, &processed); int target_diff = num_buffers_ - queued + processed; ALuint bufs[processed]; alSourceUnqueueBuffers(source_, processed, bufs); for (int i = 0; i < processed; ++i) { ALuint& buf = bufs[i]; if (0 < target_diff) { if (queue_buffer(buf)) { --target_diff; } else { alDeleteBuffers(processed - i - 1, &bufs[i+1]); break; } } else alDeleteBuffers(1, &buf); } if (stream_timer_.mode() != timer::invalid && 0 < target_diff) { for (int i = 0; i < target_diff; ++i) { if (!queue_buffer(AL_NONE)) break; } } if (is_playing_) { ALenum state; alGetSourcei(source_, AL_SOURCE_STATE, &state); if (state != AL_PLAYING) { // restart playing if we're stopped but supposed to be // playing... this means we didn't queue enough... alSourcePlay(source_); log_warning("not enough audio buffered"); } } } // TODO: this function is ugly bool queue_buffer(ALuint buffer) { if (buffer == AL_NONE) alGenBuffers(1, &buffer); bool looped_once = false; while (0 < queue_.size()) { sound_handle sound = queue_.front(); bool result = sound->read(buffer, sample_); if (result) { alSourceQueueBuffers(source_, 1, &buffer); return true; } else { sample_ = 0; queue_.pop_front(); if (0 < queue_.size()) { sound_handle new_sound = queue_.front(); result = new_sound->read(buffer, sample_, sound); if (result) { queue_.push_back(new_sound); alSourceQueueBuffers(source_, 1, &buffer); num_buffers_ = new_sound->num_buffers(buffer_size_); return true; } else { queue_.pop_front(); queue_.push_front(sound); } } else if (is_looping_ && !looped_once) { queue_.push_back(sound); looped_once = true; } else { break; } } } alDeleteBuffers(1, &buffer); is_playing_ = false; stream_timer_.invalidate(); return false; } void stop() { alSourceStop(source_); is_playing_ = false; stream_timer_.invalidate(); sample_ = 0; } void pause() { alSourcePause(source_); is_playing_ = false; stream_timer_.invalidate(); } void sample(const std::string& name) { stop(); deplete_stream(); queue_.clear(); queue(name); is_streaming_ = false; } void queue(const std::string& name) { sound_handle handle = resource::load(name, "ogg"); if (handle) queue_.push_back(handle); is_streaming_ = true; } bool is_playing() const { ALenum state; alGetSourcei(source_, AL_SOURCE_STATE, &state); if (state == AL_PLAYING) return true; else return is_playing_; } void loop(bool looping) { is_looping_ = looping; ALenum type; alGetSourcei(source_, AL_SOURCE_TYPE, &type); if (type != AL_STREAMING) alSourcei(source_, AL_LOOPING, is_looping_); } void stream_update(timer& timer, scalar t) { log_debug("blaaaaaaaaaaaaaaaaaaaaaaaaaaahhhhhhhhhhhhhhhhrrrrrg"); fill_stream(); } ALuint source_; bool is_playing_; bool is_looping_; bool is_streaming_; std::deque queue_; uint64_t sample_; int num_buffers_; scalar buffer_size_; timer stream_timer_; sound_backend backend_; }; sound::sound() : // pass through impl_(new sound::impl) {} sound::sound(const std::string& path) : // pass through impl_(new sound::impl(path)) {} void sound::sample(const std::string& path) { // pass through impl_->sample(path); } void sound::queue(const std::string& path) { // pass through impl_->queue(path); } void sound::play() { // pass through impl_->play(); } void sound::stop() { // pass through impl_->stop(); } void sound::pause() { // pass through impl_->pause(); } void sound::toggle() { if (is_playing()) pause(); else play(); } bool sound::is_playing() const { // pass through return impl_->is_playing(); } void sound::buffer_size(scalar seconds) { impl_->buffer_size_ = seconds; } void sound::position(const vector3& position) { float vec[3] = {position[0], position[1], position[2]}; alSourcefv(impl_->source_, AL_POSITION, vec); } void sound::velocity(const vector3& velocity) { float vec[3] = {velocity[0], velocity[1], velocity[2]}; alSourcefv(impl_->source_, AL_VELOCITY, vec); } void sound::gain(scalar gain) { alSourcef(impl_->source_, AL_GAIN, float(gain)); } void sound::pitch(scalar pitch) { alSourcef(impl_->source_, AL_PITCH, float(pitch)); } void sound::loop(bool looping) { // pass through impl_->loop(looping); } void sound::listener_position(const vector3& position) { float vec[] = {position[0], position[1], position[2]}; alListenerfv(AL_POSITION, vec); } void sound::listener_velocity(const vector3& velocity) { float vec[] = {velocity[0], velocity[1], velocity[2]}; alListenerfv(AL_VELOCITY, vec); } void sound::listener_orientation(const vector3& forward, const vector3& up) { float vec[6]; vec[0] = float(forward[0]); vec[1] = float(forward[1]); vec[2] = float(forward[2]); vec[3] = float(up[0]); vec[4] = float(up[1]); vec[5] = float(up[2]); alListenerfv(AL_ORIENTATION, vec); } } // namespace moof