diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d92d329..f44b9316 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,10 +65,12 @@ IF(APPLE) ENDIF() find_package(OpenGL REQUIRED) +find_package(OpenAL REQUIRED) find_package(Bullet REQUIRED) -find_package(SFML 2 COMPONENTS system window audio graphics network REQUIRED) +find_package(SFML 2 COMPONENTS system window graphics network REQUIRED) find_package(MAD REQUIRED) find_package(GLM REQUIRED) +find_package(LibSndFile REQUIRED) include_directories("${GLM_INCLUDE_DIRS}") diff --git a/cmake/modules/FindLibSndFile.cmake b/cmake/modules/FindLibSndFile.cmake new file mode 100644 index 00000000..0d4e5dc8 --- /dev/null +++ b/cmake/modules/FindLibSndFile.cmake @@ -0,0 +1,34 @@ +# - Try to find libsndfile +# Once done, this will define +# +# LIBSNDFILE_FOUND - system has libsndfile +# LIBSNDFILE_INCLUDE_DIRS - the libsndfile include directories +# LIBSNDFILE_LIBRARIES - link these to use libsndfile + +# Use pkg-config to get hints about paths +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBSNDFILE_PKGCONF sndfile) +endif(PKG_CONFIG_FOUND) + +# Include dir +find_path(LIBSNDFILE_INCLUDE_DIR + NAMES sndfile.h + PATHS ${LIBSNDFILE_PKGCONF_INCLUDE_DIRS} +) + +# Library +find_library(LIBSNDFILE_LIBRARY + NAMES sndfile libsndfile-1 + PATHS ${LIBSNDFILE_PKGCONF_LIBRARY_DIRS} +) + +find_package(PackageHandleStandardArgs) +find_package_handle_standard_args(LibSndFile DEFAULT_MSG LIBSNDFILE_LIBRARY LIBSNDFILE_INCLUDE_DIR) + +if(LIBSNDFILE_FOUND) + set(LIBSNDFILE_LIBRARIES ${LIBSNDFILE_LIBRARY}) + set(LIBSNDFILE_INCLUDE_DIRS ${LIBSNDFILE_INCLUDE_DIR}) +endif(LIBSNDFILE_FOUND) + +mark_as_advanced(LIBSNDFILE_LIBRARY LIBSNDFILE_LIBRARIES LIBSNDFILE_INCLUDE_DIR LIBSNDFILE_INCLUDE_DIRS) diff --git a/rwengine/CMakeLists.txt b/rwengine/CMakeLists.txt index 9174a1d4..b75dd4c3 100644 --- a/rwengine/CMakeLists.txt +++ b/rwengine/CMakeLists.txt @@ -13,12 +13,16 @@ target_link_libraries(rwengine rwlib ${MAD_LIBRARY} ${SFML_LIBRARIES} + ${LIBSNDFILE_LIBRARY} + ${OPENAL_LIBRARY} ${OPENRW_PLATFORM_LIBS}) include_directories(SYSTEM ${BULLET_INCLUDE_DIR} ${MAD_INCLUDE_DIR} ${SFML_INCLUDE_DIR} + ${LIBSNDFILE_INCLUDE_DIR} + ${OPENAL_INCLUDE_DIR} ) include_directories( diff --git a/rwengine/include/audio/MADStream.hpp b/rwengine/include/audio/MADStream.hpp index 7531f887..d58e2ef5 100644 --- a/rwengine/include/audio/MADStream.hpp +++ b/rwengine/include/audio/MADStream.hpp @@ -7,30 +7,17 @@ #include #include #include -#include #include #include #include +#include +#include +#include "audio/alCheck.hpp" -static inline -signed int scale(mad_fixed_t sample) -{ - /* round */ - sample += (1L << (MAD_F_FRACBITS - 16)); - - /* clip */ - if (sample >= MAD_F_ONE) - sample = MAD_F_ONE - 1; - else if (sample < -MAD_F_ONE) - sample = -MAD_F_ONE; - - /* quantize */ - return sample >> (MAD_F_FRACBITS + 1 - 16); -} #include -class MADStream : public sf::SoundStream +class MADStream { mad_decoder mDecoder; unsigned int mMadSampleRate; @@ -40,131 +27,34 @@ class MADStream : public sf::SoundStream unsigned int mReadProgress; std::vector mCurrentSamples; - static mad_flow ms_header(void* user, mad_header const* header) - { - MADStream* stream = static_cast(user); + /** + * The number of OpenAL buffers is arbitrary, but due to the kind of small + * buffer/audio sample size, we need a bunch of them so the computer can + * keep up with filling them. + */ + constexpr static size_t numALbuffers = 8; + ALuint buffers[numALbuffers]; + ALuint unqueuedBuffers[numALbuffers]; + size_t numFreeBuffers = numALbuffers; + size_t currentBuffer = 0; + ALuint alSource; - stream->mMadSampleRate = header->samplerate; - // see enum mad_mode - stream->mMadChannels = header->mode + 1; + bool stopped = false; - return MAD_FLOW_CONTINUE; - } - - static mad_flow ms_input(void* user, mad_stream* stream) - { - MADStream* self = static_cast(user); - - if(! self->mReadProgress ) { - return MAD_FLOW_STOP; - } - - auto rd = self->mReadProgress; - self->mReadProgress = 0; - - mad_stream_buffer(stream, self->mFdm, rd); - - return MAD_FLOW_CONTINUE; - } - - static mad_flow ms_output(void* user, mad_header const* header, - mad_pcm* pcm) - { - RW_UNUSED(header); - - MADStream* self = static_cast(user); - - int nsamples = pcm->length; - mad_fixed_t const *left, *right; - - left = pcm->samples[0]; - right = pcm->samples[1]; - - int s = 0; - while( (nsamples) -- ) { - signed int sample = *left++; - self->mCurrentSamples.push_back(scale(sample)); - - sample = *right++; - self->mCurrentSamples.push_back(scale(sample)); - s++; - } - - return MAD_FLOW_CONTINUE; - } - - static mad_flow ms_error(void* user, mad_stream* stream, mad_frame* frame) - { - RW_UNUSED(user); - RW_UNUSED(frame); - - sf::err() << "libmad error: " << mad_stream_errorstr(stream); - return MAD_FLOW_BREAK; - } - - virtual bool onGetData(sf::SoundStream::Chunk& data) - { - data.samples = mCurrentSamples.data(); - data.sampleCount = mCurrentSamples.size(); - - return false; - } - - virtual void onSeek(sf::Time timeOffset) - { - RW_UNUSED(timeOffset); - /// @todo support seeking. - } + static inline signed int scale(mad_fixed_t sample); + static mad_flow ms_header(void* user, mad_header const* header); + static mad_flow ms_input(void* user, mad_stream* stream); + static mad_flow ms_output(void* user, mad_header const* header, mad_pcm* pcm); + static mad_flow ms_error(void* user, mad_stream* stream, mad_frame* frame); public: - MADStream() - : mFdm(nullptr) - { + MADStream(); + ~MADStream(); - } - - ~MADStream() - { - if( mFdm ) - { - munmap( mFdm, mStat.st_size ); - mad_decoder_finish(&mDecoder); - } - } - - bool openFromFile(const std::string& loc) - { - if( mFdm ) { - munmap( mFdm, mStat.st_size ); - mCurrentSamples.clear(); - mad_decoder_finish(&mDecoder); - } - - int fd = ::open(loc.c_str(), O_RDONLY); - - if( fstat(fd, &mStat) == -1 || mStat.st_size == 0) { - std::cerr << "Fstat failed (" << loc << ")" << std::endl; - return false; - } - - void* m = mmap(0, mStat.st_size, PROT_READ, MAP_SHARED, fd, 0); - if( m == MAP_FAILED ) { - std::cerr << "mmap failed (" << loc << ")" << std::endl; - return false; - } - mFdm = (unsigned char*)m; - mReadProgress = mStat.st_size; - - mad_decoder_init(&mDecoder, this, - ms_input, ms_header, 0, ms_output, ms_error, 0); - - mad_decoder_run(&mDecoder, MAD_DECODER_MODE_SYNC); - - this->initialize(2, mMadSampleRate); - - return true; - } + bool openFromFile(const std::string& loc); + void play(); + void stop(); }; #endif diff --git a/rwengine/include/audio/SoundManager.hpp b/rwengine/include/audio/SoundManager.hpp index 597f7d51..f7392c0b 100644 --- a/rwengine/include/audio/SoundManager.hpp +++ b/rwengine/include/audio/SoundManager.hpp @@ -1,28 +1,72 @@ #pragma once + +#include +#include +#include + +#include +#include #include -#include + +class MADStream; class SoundManager { public: SoundManager(); - void playSound(const std::string& fileName); + bool loadSound(const std::string& name, const std::string& fileName); + bool isLoaded(const std::string& name); + void playSound(const std::string& name); + void pauseSound(const std::string& name); + bool isPlaying(const std::string& name); bool playBackground(const std::string& fileName); + + bool loadMusic(const std::string& name, const std::string& fileName); + void playMusic(const std::string& name); + void stopMusic(const std::string& name); void pause(bool p); private: - struct PlayingSound + class SoundSource { - sf::Sound sound; - sf::SoundBuffer buffer; + friend class SoundManager; + friend class SoundBuffer; + public: + void loadFromFile(const std::string& filename); + private: + SF_INFO fileInfo; + SNDFILE* file; + std::vector data; }; - std::vector sounds; - - sf::SoundStream* backgroundNoise; + class SoundBuffer + { + friend class SoundManager; + public: + SoundBuffer(); + bool bufferData(SoundSource& soundSource); + private: + ALuint source; + ALuint buffer; + }; -}; \ No newline at end of file + struct Sound + { + SoundSource source; + SoundBuffer buffer; + bool isLoaded = false; + }; + + bool initializeOpenAL(); + + ALCcontext* alContext; + ALCdevice* alDevice; + + std::map sounds; + std::map musics; + std::string backgroundNoise; +}; diff --git a/rwengine/include/audio/alCheck.hpp b/rwengine/include/audio/alCheck.hpp new file mode 100644 index 00000000..11744988 --- /dev/null +++ b/rwengine/include/audio/alCheck.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +void checkALerror(const std::string& file, unsigned int line); + +#if RW_DEBUG + #define alCheck(stmt) do { stmt; checkALerror(__FILE__, __LINE__); } while(0) +#else + #define alCheck(stmt) stmt +#endif diff --git a/rwengine/include/engine/GameData.hpp b/rwengine/include/engine/GameData.hpp index b75ed36a..0728492a 100644 --- a/rwengine/include/engine/GameData.hpp +++ b/rwengine/include/engine/GameData.hpp @@ -148,7 +148,7 @@ public: void loadWeaponDAT(const std::string& name); bool loadAudioStream(const std::string& name); - bool loadAudioClip(const std::string& name); + bool loadAudioClip(const std::string& name, const std::string& fileName); void loadSplash(const std::string& name); diff --git a/rwengine/include/engine/GameWorld.hpp b/rwengine/include/engine/GameWorld.hpp index 6a7eb3e1..ea144cef 100644 --- a/rwengine/include/engine/GameWorld.hpp +++ b/rwengine/include/engine/GameWorld.hpp @@ -306,10 +306,9 @@ public: void clearCutscene(); bool isCutsceneDone(); - sf::SoundStream* cutsceneAudio; + std::string cutsceneAudio; bool cutsceneAudioLoaded; - sf::SoundBuffer* missionAudio; - sf::Sound missionSound; + std::string missionAudio; /** * @brief loads a model into a special character slot. diff --git a/rwengine/src/audio/MADStream.cpp b/rwengine/src/audio/MADStream.cpp new file mode 100644 index 00000000..ccfb2cfb --- /dev/null +++ b/rwengine/src/audio/MADStream.cpp @@ -0,0 +1,174 @@ +#include "audio/MADStream.hpp" + +#include + +inline signed int MADStream::scale(mad_fixed_t sample) +{ + /* round */ + sample += (1L << (MAD_F_FRACBITS - 16)); + + /* clip */ + if (sample >= MAD_F_ONE) { + sample = MAD_F_ONE - 1; + } else if (sample < -MAD_F_ONE) { + sample = -MAD_F_ONE; + } + + /* quantize */ + return sample >> (MAD_F_FRACBITS + 1 - 16); +} + +mad_flow MADStream::ms_header(void* user, mad_header const* header) +{ + MADStream* stream = static_cast(user); + + stream->mMadSampleRate = header->samplerate; + // see enum mad_mode + stream->mMadChannels = header->mode + 1; + + return MAD_FLOW_CONTINUE; +} + +mad_flow MADStream::ms_input(void* user, mad_stream* stream) +{ + MADStream* self = static_cast(user); + + if ( ! self->mReadProgress ) { + return MAD_FLOW_STOP; + } + + auto rd = self->mReadProgress; + self->mReadProgress = 0; + + mad_stream_buffer(stream, self->mFdm, rd); + + return MAD_FLOW_CONTINUE; +} + +mad_flow MADStream::ms_output(void* user, mad_header const* header, mad_pcm* pcm) +{ + RW_UNUSED(header); + + MADStream* self = static_cast(user); + + if (self->stopped) { + return MAD_FLOW_STOP; + } + + if ( ! self->numFreeBuffers) { + ALint buffersProcessed; + do { + /** + * Sleep a bit while waiting for OpenAL buffers to become available. + * The number is arbitrary and depends on the size of the buffer/audio samples, + * as well as how quickly the computer can feed more buffers into OpenAL. + */ + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + alGetSourcei(self->alSource, AL_BUFFERS_PROCESSED, &buffersProcessed); + } while (buffersProcessed <= 0); + + alCheck(alSourceUnqueueBuffers(self->alSource, buffersProcessed, self->unqueuedBuffers)); + self->numFreeBuffers += buffersProcessed; + } + + int nsamples = pcm->length; + mad_fixed_t const *left, *right; + + left = pcm->samples[0]; + right = pcm->samples[1]; + + int s = 0; + while (nsamples--) { + signed int sample = *left++; + self->mCurrentSamples.push_back(scale(sample)); + + sample = *right++; + self->mCurrentSamples.push_back(scale(sample)); + s++; + } + + alCheck(alBufferData(self->buffers[self->currentBuffer], AL_FORMAT_STEREO16, self->mCurrentSamples.data(), self->mCurrentSamples.size() * sizeof(uint16_t), pcm->samplerate)); + alCheck(alSourceQueueBuffers(self->alSource, 1, self->buffers + self->currentBuffer)); + + self->mCurrentSamples.clear(); + self->currentBuffer++; + self->currentBuffer %= numALbuffers; + self->numFreeBuffers--; + + return MAD_FLOW_CONTINUE; +} + +mad_flow MADStream::ms_error(void* user, mad_stream* stream, mad_frame* frame) +{ + RW_UNUSED(user); + RW_UNUSED(frame); + + std::cerr << "libmad error: " << mad_stream_errorstr(stream) << std::endl; + return MAD_FLOW_BREAK; +} + +MADStream::MADStream() + : mFdm(nullptr) +{ + alCheck(alGenBuffers(numALbuffers, buffers)); + alCheck(alGenSources(1, &alSource)); +} + +MADStream::~MADStream() +{ + if (mFdm) { + munmap(mFdm, mStat.st_size); + mad_decoder_finish(&mDecoder); + } +} + +bool MADStream::openFromFile(const std::string& loc) +{ + if (mFdm) { + munmap(mFdm, mStat.st_size); + mCurrentSamples.clear(); + mad_decoder_finish(&mDecoder); + } + + int fd = ::open(loc.c_str(), O_RDONLY); + + if (fstat(fd, &mStat) == -1 || mStat.st_size == 0) { + std::cerr << "Fstat failed (" << loc << ")" << std::endl; + return false; + } + + void* m = mmap(0, mStat.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (m == MAP_FAILED) { + std::cerr << "mmap failed (" << loc << ")" << std::endl; + return false; + } + + mFdm = (unsigned char*) m; + mReadProgress = mStat.st_size; + + mad_decoder_init(&mDecoder, this, + ms_input, ms_header, 0, ms_output, ms_error, 0); + + new std::thread([&] () { + mad_decoder_run(&mDecoder, MAD_DECODER_MODE_SYNC); + }); + + alCheck(alSourcef(alSource, AL_PITCH, 1)); + alCheck(alSourcef(alSource, AL_GAIN, 1)); + alCheck(alSource3f(alSource, AL_POSITION, 0, 0, 0)); + alCheck(alSource3f(alSource, AL_VELOCITY, 0, 0, 0)); + alCheck(alSourcei(alSource, AL_LOOPING, AL_FALSE)); + + return true; +} + +void MADStream::play() +{ + alCheck(alSourcePlay(alSource)); +} + +void MADStream::stop() +{ + stopped = true; + alCheck(alSourcePlay(alSource)); +} diff --git a/rwengine/src/audio/SoundManager.cpp b/rwengine/src/audio/SoundManager.cpp index eba46bb4..76ca52a2 100644 --- a/rwengine/src/audio/SoundManager.cpp +++ b/rwengine/src/audio/SoundManager.cpp @@ -1,44 +1,177 @@ #include